/* * Copyright 2013 Netflix, Inc. * * 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 feign; import com.google.common.base.Joiner; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; import dagger.Module; import dagger.Provides; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import feign.codec.StringDecoder; import org.testng.annotations.Test; import javax.inject.Named; import javax.inject.Singleton; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; import java.io.IOException; import java.lang.reflect.Type; import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; import static dagger.Provides.Type.SET; import static feign.Util.UTF_8; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; @Test // unbound wildcards are not currently injectable in dagger. @SuppressWarnings("rawtypes") public class FeignTest { interface TestInterface { @RequestLine("POST /") Response response(); @RequestLine("POST /") String post(); @RequestLine("POST /") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void login( @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password); @RequestLine("POST /") void body(List contents); @RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List contents); @RequestLine("POST /") void form( @Named("customer_name") String customer, @Named("user_name") String user, @Named("password") String password); @RequestLine("GET /{1}/{2}") Response uriParam(@Named("1") String one, URI endpoint, @Named("2") String two); @RequestLine("GET /?1={1}&2={2}") Response queryParams(@Named("1") String one, @Named("2") Iterable twos); @dagger.Module(injects = Feign.class, addsTo = Feign.Defaults.class) static class Module { @Provides Decoder defaultDecoder() { return new Decoder.Default(); } @Provides Encoder defaultEncoder() { return new Encoder() { @Override public void encode(Object object, RequestTemplate template) { if (object instanceof Map) { template.body(Joiner.on(',').withKeyValueSeparator("=").join((Map) object)); } else { template.body(object.toString()); } } }; } } } @Test public void iterableQueryParams() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.queryParams("user", Arrays.asList("apple", "pear")); assertEquals(server.takeRequest().getRequestLine(), "GET /?1=user&2=apple&2=pear HTTP/1.1"); } finally { server.shutdown(); } } interface OtherTestInterface { @RequestLine("POST /") String post(); @RequestLine("POST /") byte[] binaryResponseBody(); @RequestLine("POST /") void binaryRequestBody(byte[] contents); } @Module(library = true, overrides = true) static class RunSynchronous { @Provides @Singleton @Named("http") Executor httpExecutor() { return new Executor() { @Override public void execute(Runnable command) { command.run(); } }; } } @Test public void postTemplateParamsResolve() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.login("netflix", "denominator", "password"); assertEquals(new String(server.takeRequest().getBody(), UTF_8), "{\"customer_name\": \"netflix\", \"user_name\": \"denominator\", \"password\": \"password\"}"); } finally { server.shutdown(); } } @Test public void responseCoercesToStringBody() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); Response response = api.response(); assertTrue(response.body().isRepeatable()); assertEquals(response.body().toString(), "foo"); } finally { server.shutdown(); } } @Test public void postFormParams() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.form("netflix", "denominator", "password"); assertEquals(new String(server.takeRequest().getBody(), UTF_8), "customer_name=netflix,user_name=denominator,password=password"); } finally { server.shutdown(); } } @Test public void postBodyParam() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.body(Arrays.asList("netflix", "denominator", "password")); RecordedRequest request = server.takeRequest(); assertEquals(request.getHeader("Content-Length"), "32"); assertEquals(new String(request.getBody(), UTF_8), "[netflix, denominator, password]"); } finally { server.shutdown(); } } @Test public void postGZIPEncodedBodyParam() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.gzipBody(Arrays.asList("netflix", "denominator", "password")); RecordedRequest request = server.takeRequest(); assertNull(request.getHeader("Content-Length")); byte[] compressedBody = request.getBody(); String uncompressedBody = CharStreams.toString(CharStreams.newReaderSupplier( GZIPStreams.newInputStreamSupplier(ByteStreams.newInputStreamSupplier(compressedBody)), UTF_8)); assertEquals(uncompressedBody, "[netflix, denominator, password]"); } finally { server.shutdown(); } } @Module(library = true) static class ForwardedForInterceptor implements RequestInterceptor { @Provides(type = SET) RequestInterceptor provideThis() { return this; } @Override public void apply(RequestTemplate template) { template.header("X-Forwarded-For", "origin.host.com"); } } @Test public void singleInterceptor() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module(), new ForwardedForInterceptor()); api.post(); assertEquals(server.takeRequest().getHeader("X-Forwarded-For"), "origin.host.com"); } finally { server.shutdown(); } } @Module(library = true) static class UserAgentInterceptor implements RequestInterceptor { @Provides(type = SET) RequestInterceptor provideThis() { return this; } @Override public void apply(RequestTemplate template) { template.header("User-Agent", "Feign"); } } @Test public void multipleInterceptor() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor()); api.post(); RecordedRequest request = server.takeRequest(); assertEquals(request.getHeader("X-Forwarded-For"), "origin.host.com"); assertEquals(request.getHeader("User-Agent"), "Feign"); } finally { server.shutdown(); } } @Test public void toKeyMethodFormatsAsExpected() throws Exception { assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()"); assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class, String.class)), "TestInterface#uriParam(String,URI,String)"); } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class IllegalArgumentExceptionOn404 { @Provides @Singleton ErrorDecoder errorDecoder() { return new ErrorDecoder.Default() { @Override public Exception decode(String methodKey, Response response) { if (response.status() == 404) return new IllegalArgumentException("zone not found"); return super.decode(methodKey, response); } }; } } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "zone not found") public void canOverrideErrorDecoder() throws IOException, InterruptedException { final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setResponseCode(404).setBody("foo")); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new IllegalArgumentExceptionOn404()); api.post(); } finally { server.shutdown(); } } @Test public void retriesLostConnectionBeforeRead() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new TestInterface.Module()); api.post(); assertEquals(server.getRequestCount(), 2); } finally { server.shutdown(); } } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class DecodeFail { @Provides Decoder decoder() { return new Decoder() { @Override public Object decode(Response response, Type type) { return "fail"; } }; } } public void overrideTypeSpecificDecoder() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new DecodeFail()); assertEquals(api.post(), "fail"); } finally { server.shutdown(); assertEquals(server.getRequestCount(), 1); } } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class RetryableExceptionOnRetry { @Provides Decoder decoder() { return new StringDecoder() { @Override public Object decode(Response response, Type type) throws IOException, FeignException { String string = super.decode(response, type).toString(); if ("retry!".equals(string)) throw new RetryableException(string, null); return string; } }; } } /** * when you must parse a 2xx status to determine if the operation succeeded or not. */ public void retryableExceptionInDecoder() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("retry!".getBytes(UTF_8))); server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new RetryableExceptionOnRetry()); assertEquals(api.post(), "success!"); } finally { server.shutdown(); assertEquals(server.getRequestCount(), 2); } } @dagger.Module(overrides = true, library = true, includes = TestInterface.Module.class) static class IOEOnDecode { @Provides Decoder decoder() { return new Decoder() { @Override public Object decode(Response response, Type type) throws IOException { throw new IOException("error reading response"); } }; } } @Test(expectedExceptions = FeignException.class, expectedExceptionsMessageRegExp = "error reading response POST http://.*") public void doesntRetryAfterResponseIsSent() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), new IOEOnDecode()); api.post(); } finally { server.shutdown(); assertEquals(server.getRequestCount(), 1); } } @Module(overrides = true, includes = TestInterface.Module.class) static class TrustSSLSockets { @Provides SSLSocketFactory trustingSSLSocketFactory() { return TrustingSSLSocketFactory.get(); } } @Test public void canOverrideSSLSocketFactory() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), new TrustSSLSockets()); api.post(); } finally { server.shutdown(); } } @Module(overrides = true, includes = TrustSSLSockets.class) static class DisableHostnameVerification { @Provides HostnameVerifier acceptAllHostnameVerifier() { return new AcceptAllHostnameVerifier(); } } @Test public void canOverrideHostnameVerifier() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.useHttps(TrustingSSLSocketFactory.get("bad.example.com"), false); server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), new DisableHostnameVerification()); api.post(); } finally { server.shutdown(); } } @Test public void retriesFailedHandshake() throws IOException, InterruptedException { MockWebServer server = new MockWebServer(); server.useHttps(TrustingSSLSocketFactory.get("localhost"), false); server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); server.enqueue(new MockResponse().setBody("success!".getBytes(UTF_8))); server.play(); try { TestInterface api = Feign.create(TestInterface.class, "https://localhost:" + server.getPort(), new TestInterface.Module(), new TrustSSLSockets()); api.post(); assertEquals(server.getRequestCount(), 2); } finally { server.shutdown(); } } @Test public void equalsAndHashCodeWork() { TestInterface i1 = Feign.create(TestInterface.class, "http://localhost:8080", new TestInterface.Module()); TestInterface i2 = Feign.create(TestInterface.class, "http://localhost:8080", new TestInterface.Module()); TestInterface i3 = Feign.create(TestInterface.class, "http://localhost:8888", new TestInterface.Module()); OtherTestInterface i4 = Feign.create(OtherTestInterface.class, "http://localhost:8080", new TestInterface.Module()); assertTrue(i1.equals(i1)); assertTrue(i1.equals(i2)); assertFalse(i1.equals(i3)); assertFalse(i1.equals(i4)); assertEquals(i1.hashCode(), i1.hashCode()); assertEquals(i1.hashCode(), i2.hashCode()); } @Test public void decodeLogicSupportsByteArray() throws Exception { byte[] expectedResponse = {12, 34, 56}; final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse().setBody(expectedResponse)); server.play(); try { OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); byte[] actualResponse = api.binaryResponseBody(); assertEquals(actualResponse, expectedResponse); } finally { server.shutdown(); } } @Test public void encodeLogicSupportsByteArray() throws Exception { byte[] expectedRequest = {12, 34, 56}; final MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse()); server.play(); try { OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); api.binaryRequestBody(expectedRequest); byte[] actualRequest = server.takeRequest().getBody(); assertEquals(actualRequest, expectedRequest); } finally { server.shutdown(); } } }