/*
* 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<String> contents);
@RequestLine("POST /") @Headers("Content-Encoding: gzip") void gzipBody(List<String> 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<String> 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();
}
}
}