package feign.client; import java.io.ByteArrayInputStream; import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import feign.Client; import feign.Feign.Builder; import feign.FeignException; import feign.Headers; import feign.Logger; import feign.Param; import feign.RequestLine; import feign.Response; import feign.Util; import feign.assertj.MockWebServerAssertions; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static feign.Util.UTF_8; /** * {@link AbstractClientTest} can be extended to run a set of tests against any {@link Client} implementation. */ public abstract class AbstractClientTest { @Rule public final ExpectedException thrown = ExpectedException.none(); @Rule public final MockWebServer server = new MockWebServer(); /** * Create a Feign {@link Builder} with a client configured */ public abstract Builder newBuilder(); /** * Some client implementation tests should override this * test if the PATCH operation is unsupported. */ @Test public void testPatch() throws Exception { server.enqueue(new MockResponse().setBody("foo")); server.enqueue(new MockResponse()); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); assertEquals("foo", api.patch("")); MockWebServerAssertions.assertThat(server.takeRequest()) .hasHeaders("Accept: text/plain", "Content-Length: 0") // Note: OkHttp adds content length. .hasNoHeaderNamed("Content-Type") .hasMethod("PATCH"); } @Test public void parsesRequestAndResponse() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar")); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.post("foo"); assertThat(response.status()).isEqualTo(200); assertThat(response.reason()).isEqualTo("OK"); assertThat(response.headers()) .containsEntry("Content-Length", asList("3")) .containsEntry("Foo", asList("Bar")); assertThat(response.body().asInputStream()) .hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8))); MockWebServerAssertions.assertThat(server.takeRequest()).hasMethod("POST") .hasPath("/?foo=bar&foo=baz&qux=") .hasHeaders("Foo: Bar", "Foo: Baz", "Qux: ", "Accept: */*", "Content-Length: 3") .hasBody("foo"); } @Test public void reasonPhraseIsOptional() throws IOException, InterruptedException { server.enqueue(new MockResponse().setStatus("HTTP/1.1 " + 200)); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.post("foo"); assertThat(response.status()).isEqualTo(200); assertThat(response.reason()).isNullOrEmpty(); } @Test public void parsesErrorResponse() throws IOException, InterruptedException { thrown.expect(FeignException.class); thrown.expectMessage("status 500 reading TestInterface#get(); content:\n" + "ARGHH"); server.enqueue(new MockResponse().setResponseCode(500).setBody("ARGHH")); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); api.get(); } @Test public void safeRebuffering() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = newBuilder() .logger(new Logger(){ @Override protected void log(String configKey, String format, Object... args) { } }) .logLevel(Logger.Level.FULL) // rebuffers the body .target(TestInterface.class, "http://localhost:" + server.getPort()); api.post("foo"); } /** This shows that is a no-op or otherwise doesn't cause an NPE when there's no content. */ @Test public void safeRebuffering_noContent() throws IOException, InterruptedException { server.enqueue(new MockResponse().setResponseCode(204)); TestInterface api = newBuilder() .logger(new Logger(){ @Override protected void log(String configKey, String format, Object... args) { } }) .logLevel(Logger.Level.FULL) // rebuffers the body .target(TestInterface.class, "http://localhost:" + server.getPort()); api.post("foo"); } @Test public void noResponseBodyForPost() { server.enqueue(new MockResponse()); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); api.noPostBody(); } @Test public void noResponseBodyForPut() { server.enqueue(new MockResponse()); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); api.noPutBody(); } @Test public void parsesResponseMissingLength() throws IOException, InterruptedException { server.enqueue(new MockResponse().setChunkedBody("foo", 1)); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.post("testing"); assertThat(response.status()).isEqualTo(200); assertThat(response.reason()).isEqualTo("OK"); assertThat(response.body().length()).isNull(); assertThat(response.body().asInputStream()) .hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8))); } @Test public void postWithSpacesInPath() throws IOException, InterruptedException { server.enqueue(new MockResponse().setBody("foo")); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.post("current documents", "foo"); MockWebServerAssertions.assertThat(server.takeRequest()).hasMethod("POST") .hasPath("/path/current%20documents/resource") .hasBody("foo"); } @Test public void testVeryLongResponseNullLength() throws Exception { server.enqueue(new MockResponse() .setBody("AAAAAAAA") .addHeader("Content-Length", Long.MAX_VALUE)); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.post("foo"); // Response length greater than Integer.MAX_VALUE should be null assertThat(response.body().length()).isNull(); } @Test public void testResponseLength() throws Exception { server.enqueue(new MockResponse() .setBody("test")); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Integer expected = 4; Response response = api.post(""); Integer actual = response.body().length(); assertEquals(expected, actual); } @Test public void testContentTypeWithCharset() throws Exception { server.enqueue(new MockResponse() .setBody("AAAAAAAA")); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.postWithContentType("foo", "text/plain;charset=utf-8"); // Response length should not be null assertEquals("AAAAAAAA", Util.toString(response.body().asReader())); } @Test public void testContentTypeWithoutCharset() throws Exception { server.enqueue(new MockResponse() .setBody("AAAAAAAA")); TestInterface api = newBuilder() .target(TestInterface.class, "http://localhost:" + server.getPort()); Response response = api.postWithContentType("foo", "text/plain"); // Response length should not be null assertEquals("AAAAAAAA", Util.toString(response.body().asReader())); } public interface TestInterface { @RequestLine("POST /?foo=bar&foo=baz&qux=") @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: text/plain"}) Response post(String body); @RequestLine("POST /path/{to}/resource") @Headers("Accept: text/plain") Response post(@Param("to") String to, String body); @RequestLine("GET /") @Headers("Accept: text/plain") String get(); @RequestLine("PATCH /") @Headers("Accept: text/plain") String patch(String body); @RequestLine("POST") String noPostBody(); @RequestLine("PUT") String noPutBody(); @RequestLine("POST /?foo=bar&foo=baz&qux=") @Headers({"Foo: Bar", "Foo: Baz", "Qux: ", "Content-Type: {contentType}"}) Response postWithContentType(String body, @Param("contentType") String contentType); } }