/* * 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.gson.reflect.TypeToken; import org.assertj.core.api.Fail; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.net.URI; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import static feign.assertj.FeignAssertions.assertThat; import static java.util.Arrays.asList; import static org.assertj.core.data.MapEntry.entry; /** * Tests interfaces defined per {@link Contract.Default} are interpreted into expected {@link feign * .RequestTemplate template} instances. */ public class DefaultContractTest { @Rule public final ExpectedException thrown = ExpectedException.none(); Contract.Default contract = new Contract.Default(); @Test public void httpMethods() throws Exception { assertThat(parseAndValidateMetadata(Methods.class, "post").template()) .hasMethod("POST"); assertThat(parseAndValidateMetadata(Methods.class, "put").template()) .hasMethod("PUT"); assertThat(parseAndValidateMetadata(Methods.class, "get").template()) .hasMethod("GET"); assertThat(parseAndValidateMetadata(Methods.class, "delete").template()) .hasMethod("DELETE"); } @Test public void bodyParamIsGeneric() throws Exception { MethodMetadata md = parseAndValidateMetadata(BodyParams.class, "post", List.class); assertThat(md.bodyIndex()) .isEqualTo(0); assertThat(md.bodyType()) .isEqualTo(new TypeToken<List<String>>() { }.getType()); } @Test public void bodyParamWithPathParam() throws Exception { MethodMetadata md = parseAndValidateMetadata(BodyParams.class, "post", int.class, List.class); assertThat(md.bodyIndex()) .isEqualTo(1); assertThat(md.indexToName()).containsOnly( entry(0, asList("id")) ); } @Test public void tooManyBodies() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Method has too many Body"); parseAndValidateMetadata(BodyParams.class, "tooMany", List.class, List.class); } @Test public void customMethodWithoutPath() throws Exception { assertThat(parseAndValidateMetadata(CustomMethod.class, "patch").template()) .hasMethod("PATCH") .hasUrl(""); } @Test public void queryParamsInPathExtract() throws Exception { assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "none").template()) .hasUrl("/") .hasQueries(); assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "one").template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")) ); assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "two").template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")) ); assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "three").template()) .hasUrl("/") .hasQueries( entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")), entry("limit", asList("1")) ); assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "twoAndOneEmpty").template()) .hasUrl("/") .hasQueries( entry("flag", asList(new String[]{null})), entry("Action", asList("GetUser")), entry("Version", asList("2010-05-08")) ); assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "oneEmpty").template()) .hasUrl("/") .hasQueries( entry("flag", asList(new String[]{null})) ); assertThat(parseAndValidateMetadata(WithQueryParamsInPath.class, "twoEmpty").template()) .hasUrl("/") .hasQueries( entry("flag", asList(new String[]{null})), entry("NoErrors", asList(new String[]{null})) ); } @Test public void bodyWithoutParameters() throws Exception { MethodMetadata md = parseAndValidateMetadata(BodyWithoutParameters.class, "post"); assertThat(md.template()) .hasBody("<v01:getAccountsListOfUser/>"); } @Test public void headersOnMethodAddsContentTypeHeader() throws Exception { MethodMetadata md = parseAndValidateMetadata(BodyWithoutParameters.class, "post"); assertThat(md.template()) .hasHeaders( entry("Content-Type", asList("application/xml")), entry("Content-Length", asList(String.valueOf(md.template().body().length))) ); } @Test public void headersOnTypeAddsContentTypeHeader() throws Exception { MethodMetadata md = parseAndValidateMetadata(HeadersOnType.class, "post"); assertThat(md.template()) .hasHeaders( entry("Content-Type", asList("application/xml")), entry("Content-Length", asList(String.valueOf(md.template().body().length))) ); } @Test public void withPathAndURIParam() throws Exception { MethodMetadata md = parseAndValidateMetadata(WithURIParam.class, "uriParam", String.class, URI.class, String.class); assertThat(md.indexToName()) .containsExactly( entry(0, asList("1")), // Skips 1 as it is a url index! entry(2, asList("2")) ); assertThat(md.urlIndex()).isEqualTo(1); } @Test public void pathAndQueryParams() throws Exception { MethodMetadata md = parseAndValidateMetadata(WithPathAndQueryParams.class, "recordsByNameAndType", int.class, String.class, String.class); assertThat(md.template()) .hasQueries(entry("name", asList("{name}")), entry("type", asList("{type}"))); assertThat(md.indexToName()).containsExactly( entry(0, asList("domainId")), entry(1, asList("name")), entry(2, asList("type")) ); } @Test public void bodyWithTemplate() throws Exception { MethodMetadata md = parseAndValidateMetadata(FormParams.class, "login", String.class, String.class, String.class); assertThat(md.template()) .hasBodyTemplate( "%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D"); } @Test public void formParamsParseIntoIndexToName() throws Exception { MethodMetadata md = parseAndValidateMetadata(FormParams.class, "login", String.class, String.class, String.class); assertThat(md.formParams()) .containsExactly("customer_name", "user_name", "password"); assertThat(md.indexToName()).containsExactly( entry(0, asList("customer_name")), entry(1, asList("user_name")), entry(2, asList("password")) ); } /** * Body type is only for the body param. */ @Test public void formParamsDoesNotSetBodyType() throws Exception { MethodMetadata md = parseAndValidateMetadata(FormParams.class, "login", String.class, String.class, String.class); assertThat(md.bodyType()).isNull(); } @Test public void headerParamsParseIntoIndexToName() throws Exception { MethodMetadata md = parseAndValidateMetadata(HeaderParams.class, "logout", String.class); assertThat(md.template()) .hasHeaders(entry("Auth-Token", asList("{authToken}", "Foo"))); assertThat(md.indexToName()) .containsExactly(entry(0, asList("authToken"))); assertThat(md.formParams()).isEmpty(); } @Test public void headerParamsParseIntoIndexToNameNotAtStart() throws Exception { MethodMetadata md = parseAndValidateMetadata(HeaderParamsNotAtStart.class, "logout", String.class); assertThat(md.template()) .hasHeaders(entry("Authorization", asList("Bearer {authToken}", "Foo"))); assertThat(md.indexToName()) .containsExactly(entry(0, asList("authToken"))); assertThat(md.formParams()).isEmpty(); } @Test public void customExpander() throws Exception { MethodMetadata md = parseAndValidateMetadata(CustomExpander.class, "date", Date.class); assertThat(md.indexToExpanderClass()) .containsExactly(entry(0, DateToMillis.class)); } @Test public void queryMap() throws Exception { MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMap", Map.class); assertThat(md.queryMapIndex()).isEqualTo(0); } @Test public void queryMapEncodedDefault() throws Exception { MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMap", Map.class); assertThat(md.queryMapEncoded()).isFalse(); } @Test public void queryMapEncodedTrue() throws Exception { MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMapEncoded", Map.class); assertThat(md.queryMapEncoded()).isTrue(); } @Test public void queryMapEncodedFalse() throws Exception { MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMapNotEncoded", Map.class); assertThat(md.queryMapEncoded()).isFalse(); } @Test public void queryMapMapSubclass() throws Exception { MethodMetadata md = parseAndValidateMetadata(QueryMapTestInterface.class, "queryMapMapSubclass", SortedMap.class); assertThat(md.queryMapIndex()).isEqualTo(0); } @Test public void onlyOneQueryMapAnnotationPermitted() throws Exception { try { parseAndValidateMetadata(QueryMapTestInterface.class, "multipleQueryMap", Map.class, Map.class); Fail.failBecauseExceptionWasNotThrown(IllegalStateException.class); } catch (IllegalStateException ex) { assertThat(ex).hasMessage("QueryMap annotation was present on multiple parameters."); } } @Test public void queryMapMustBeInstanceOfMap() throws Exception { try { parseAndValidateMetadata(QueryMapTestInterface.class, "nonMapQueryMap", String.class); Fail.failBecauseExceptionWasNotThrown(IllegalStateException.class); } catch (IllegalStateException ex) { assertThat(ex).hasMessage("QueryMap parameter must be a Map: class java.lang.String"); } } @Test public void slashAreEncodedWhenNeeded() throws Exception { MethodMetadata md = parseAndValidateMetadata(SlashNeedToBeEncoded.class, "getQueues", String.class); assertThat(md.template().decodeSlash()).isFalse(); md = parseAndValidateMetadata(SlashNeedToBeEncoded.class, "getZone", String.class); assertThat(md.template().decodeSlash()).isTrue(); } @Test public void onlyOneHeaderMapAnnotationPermitted() throws Exception { try { parseAndValidateMetadata(HeaderMapInterface.class, "multipleHeaderMap", Map.class, Map.class); Fail.failBecauseExceptionWasNotThrown(IllegalStateException.class); } catch (IllegalStateException ex) { assertThat(ex).hasMessage("HeaderMap annotation was present on multiple parameters."); } } interface Methods { @RequestLine("POST /") void post(); @RequestLine("PUT /") void put(); @RequestLine("GET /") void get(); @RequestLine("DELETE /") void delete(); } interface BodyParams { @RequestLine("POST") Response post(List<String> body); @RequestLine("PUT /offers/{id}") void post(@Param("id") int id, List<String> body); @RequestLine("POST") Response tooMany(List<String> body, List<String> body2); } interface CustomMethod { @RequestLine("PATCH") Response patch(); } interface WithQueryParamsInPath { @RequestLine("GET /") Response none(); @RequestLine("GET /?Action=GetUser") Response one(); @RequestLine("GET /?Action=GetUser&Version=2010-05-08") Response two(); @RequestLine("GET /?Action=GetUser&Version=2010-05-08&limit=1") Response three(); @RequestLine("GET /?flag&Action=GetUser&Version=2010-05-08") Response twoAndOneEmpty(); @RequestLine("GET /?flag") Response oneEmpty(); @RequestLine("GET /?flag&NoErrors") Response twoEmpty(); } interface BodyWithoutParameters { @RequestLine("POST /") @Headers("Content-Type: application/xml") @Body("<v01:getAccountsListOfUser/>") Response post(); } @Headers("Content-Type: application/xml") interface HeadersOnType { @RequestLine("POST /") @Body("<v01:getAccountsListOfUser/>") Response post(); } interface WithURIParam { @RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); } interface WithPathAndQueryParams { @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") Response recordsByNameAndType(@Param("domainId") int id, @Param("name") String nameFilter, @Param("type") String typeFilter); } interface FormParams { @RequestLine("POST /") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") void login( @Param("customer_name") String customer, @Param("user_name") String user, @Param("password") String password); } interface HeaderMapInterface { @RequestLine("POST /") void multipleHeaderMap(@HeaderMap Map<String, String> headers, @HeaderMap Map<String,String> queries); } interface HeaderParams { @RequestLine("POST /") @Headers({"Auth-Token: {authToken}", "Auth-Token: Foo"}) void logout(@Param("authToken") String token); } interface HeaderParamsNotAtStart { @RequestLine("POST /") @Headers({"Authorization: Bearer {authToken}", "Authorization: Foo"}) void logout(@Param("authToken") String token); } interface CustomExpander { @RequestLine("POST /?date={date}") void date(@Param(value = "date", expander = DateToMillis.class) Date date); } class DateToMillis implements Param.Expander { @Override public String expand(Object value) { return String.valueOf(((Date) value).getTime()); } } interface QueryMapTestInterface { @RequestLine("POST /") void queryMap(@QueryMap Map<String, String> queryMap); @RequestLine("POST /") void queryMapMapSubclass(@QueryMap SortedMap<String, String> queryMap); @RequestLine("POST /") void queryMapEncoded(@QueryMap(encoded = true) Map<String, String> queryMap); @RequestLine("POST /") void queryMapNotEncoded(@QueryMap(encoded = false) Map<String, String> queryMap); // invalid @RequestLine("POST /") void multipleQueryMap(@QueryMap Map<String, String> mapOne, @QueryMap Map<String, String> mapTwo); // invalid @RequestLine("POST /") void nonMapQueryMap(@QueryMap String notAMap); } interface SlashNeedToBeEncoded { @RequestLine(value = "GET /api/queues/{vhost}", decodeSlash = false) String getQueues(@Param("vhost") String vhost); @RequestLine("GET /api/{zoneId}") String getZone(@Param("ZoneId") String vhost); } @Headers("Foo: Bar") interface SimpleParameterizedBaseApi<M> { @RequestLine("GET /api/{zoneId}") M get(@Param("key") String key); } interface SimpleParameterizedApi extends SimpleParameterizedBaseApi<String> { } @Test public void simpleParameterizedBaseApi() throws Exception { List<MethodMetadata> md = contract.parseAndValidatateMetadata(SimpleParameterizedApi.class); assertThat(md).hasSize(1); assertThat(md.get(0).configKey()) .isEqualTo("SimpleParameterizedApi#get(String)"); assertThat(md.get(0).returnType()) .isEqualTo(String.class); assertThat(md.get(0).template()) .hasHeaders(entry("Foo", asList("Bar"))); } @Test public void parameterizedApiUnsupported() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Parameterized types unsupported: SimpleParameterizedBaseApi"); contract.parseAndValidatateMetadata(SimpleParameterizedBaseApi.class); } interface OverrideParameterizedApi extends SimpleParameterizedBaseApi<String> { @Override @RequestLine("GET /api/{zoneId}") String get(@Param("key") String key); } @Test public void overrideBaseApiUnsupported() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Overrides unsupported: OverrideParameterizedApi#get(String)"); contract.parseAndValidatateMetadata(OverrideParameterizedApi.class); } interface Child<T> extends SimpleParameterizedBaseApi<List<T>> { } interface GrandChild extends Child<String> { } @Test public void onlySingleLevelInheritanceSupported() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("Only single-level inheritance supported: GrandChild"); contract.parseAndValidatateMetadata(GrandChild.class); } @Headers("Foo: Bar") interface ParameterizedBaseApi<K, M> { @RequestLine("GET /api/{key}") Entity<K, M> get(@Param("key") K key); @RequestLine("POST /api") Entities<K, M> getAll(Keys<K> keys); } static class Keys<K> { List<K> keys; } static class Entity<K, M> { K key; M model; } static class Entities<K, M> { private List<Entity<K, M>> entities; } @Headers("Version: 1") interface ParameterizedApi extends ParameterizedBaseApi<String, Long> { } @Test public void parameterizedBaseApi() throws Exception { List<MethodMetadata> md = contract.parseAndValidatateMetadata(ParameterizedApi.class); Map<String, MethodMetadata> byConfigKey = new LinkedHashMap<String, MethodMetadata>(); for (MethodMetadata m : md) { byConfigKey.put(m.configKey(), m); } assertThat(byConfigKey) .containsOnlyKeys("ParameterizedApi#get(String)", "ParameterizedApi#getAll(Keys)"); assertThat(byConfigKey.get("ParameterizedApi#get(String)").returnType()) .isEqualTo(new TypeToken<Entity<String, Long>>() { }.getType()); assertThat(byConfigKey.get("ParameterizedApi#get(String)").template()).hasHeaders( entry("Version", asList("1")), entry("Foo", asList("Bar")) ); assertThat(byConfigKey.get("ParameterizedApi#getAll(Keys)").returnType()) .isEqualTo(new TypeToken<Entities<String, Long>>() { }.getType()); assertThat(byConfigKey.get("ParameterizedApi#getAll(Keys)").bodyType()) .isEqualTo(new TypeToken<Keys<String>>() { }.getType()); assertThat(byConfigKey.get("ParameterizedApi#getAll(Keys)").template()).hasHeaders( entry("Version", asList("1")), entry("Foo", asList("Bar")) ); } @Headers("Authorization: {authHdr}") interface ParameterizedHeaderExpandApi { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); } @Test public void parameterizedHeaderExpandApi() throws Exception { List<MethodMetadata> md = contract.parseAndValidatateMetadata(ParameterizedHeaderExpandApi.class); assertThat(md).hasSize(1); assertThat(md.get(0).configKey()) .isEqualTo("ParameterizedHeaderExpandApi#getZone(String,String)"); assertThat(md.get(0).returnType()) .isEqualTo(String.class); assertThat(md.get(0).template()) .hasHeaders(entry("Authorization", asList("{authHdr}")), entry("Accept", asList("application/json"))); // Ensure that the authHdr expansion was properly detected and did not create a formParam assertThat(md.get(0).formParams()) .isEmpty(); } @Test public void parameterizedHeaderNotStartingWithCurlyBraceExpandApi() throws Exception { List<MethodMetadata> md = contract.parseAndValidatateMetadata( ParameterizedHeaderNotStartingWithCurlyBraceExpandApi.class); assertThat(md).hasSize(1); assertThat(md.get(0).configKey()) .isEqualTo("ParameterizedHeaderNotStartingWithCurlyBraceExpandApi#getZone(String,String)"); assertThat(md.get(0).returnType()) .isEqualTo(String.class); assertThat(md.get(0).template()) .hasHeaders(entry("Authorization", asList("Bearer {authHdr}")), entry("Accept", asList("application/json"))); // Ensure that the authHdr expansion was properly detected and did not create a formParam assertThat(md.get(0).formParams()) .isEmpty(); } @Headers("Authorization: Bearer {authHdr}") interface ParameterizedHeaderNotStartingWithCurlyBraceExpandApi { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); } @Headers("Authorization: {authHdr}") interface ParameterizedHeaderBase { } interface ParameterizedHeaderExpandInheritedApi extends ParameterizedHeaderBase { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZoneAccept(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); @RequestLine("GET /api/{zoneId}") String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); } @Test public void parameterizedHeaderExpandApiBaseClass() throws Exception { List<MethodMetadata> mds = contract.parseAndValidatateMetadata(ParameterizedHeaderExpandInheritedApi.class); Map<String, MethodMetadata> byConfigKey = new LinkedHashMap<String, MethodMetadata>(); for (MethodMetadata m : mds) { byConfigKey.put(m.configKey(), m); } assertThat(byConfigKey) .containsOnlyKeys("ParameterizedHeaderExpandInheritedApi#getZoneAccept(String,String)", "ParameterizedHeaderExpandInheritedApi#getZone(String,String)"); MethodMetadata md = byConfigKey.get("ParameterizedHeaderExpandInheritedApi#getZoneAccept(String,String)"); assertThat(md.returnType()) .isEqualTo(String.class); assertThat(md.template()) .hasHeaders(entry("Authorization", asList("{authHdr}")), entry("Accept", asList("application/json"))); // Ensure that the authHdr expansion was properly detected and did not create a formParam assertThat(md.formParams()) .isEmpty(); md = byConfigKey.get("ParameterizedHeaderExpandInheritedApi#getZone(String,String)"); assertThat(md.returnType()) .isEqualTo(String.class); assertThat(md.template()) .hasHeaders(entry("Authorization", asList("{authHdr}"))); assertThat(md.formParams()) .isEmpty(); } private MethodMetadata parseAndValidateMetadata(Class<?> targetType, String method, Class<?>... parameterTypes) throws NoSuchMethodException { return contract.parseAndValidateMetadata(targetType, targetType.getMethod(method, parameterTypes)); } interface MissingMethod { @RequestLine("/path?queryParam={queryParam}") Response updateSharing(@Param("queryParam") long queryParam, String bodyParam); } /** Let's help folks not lose time when they mistake request line for a URI! */ @Test public void missingMethod() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage("RequestLine annotation didn't start with an HTTP verb on method updateSharing"); contract.parseAndValidatateMetadata(MissingMethod.class); } interface StaticMethodOnInterface { @RequestLine("GET /api/{key}") String get(@Param("key") String key); static String staticMethod() { return "value"; } } @Test public void staticMethodsOnInterfaceIgnored() throws Exception { List<MethodMetadata> mds = contract.parseAndValidatateMetadata(StaticMethodOnInterface.class); assertThat(mds).hasSize(1); MethodMetadata md = mds.get(0); assertThat(md.configKey()).isEqualTo("StaticMethodOnInterface#get(String)"); } interface DefaultMethodOnInterface { @RequestLine("GET /api/{key}") String get(@Param("key") String key); default String defaultGet(String key) { return get(key); } } @Test public void defaultMethodsOnInterfaceIgnored() throws Exception { List<MethodMetadata> mds = contract.parseAndValidatateMetadata(DefaultMethodOnInterface.class); assertThat(mds).hasSize(1); MethodMetadata md = mds.get(0); assertThat(md.configKey()).isEqualTo("DefaultMethodOnInterface#get(String)"); } interface SubstringQuery { @RequestLine("GET /_search?q=body:{body}") String paramIsASubstringOfAQuery(@Param("body") String body); } @Test public void paramIsASubstringOfAQuery() throws Exception { List<MethodMetadata> mds = contract.parseAndValidatateMetadata(SubstringQuery.class); assertThat(mds.get(0).template().queries()).containsExactly( entry("q", asList("body:{body}")) ); assertThat(mds.get(0).formParams()).isEmpty(); // Prevent issue 424 } }