/* * Copyright (C) 2015 AppTik Project * * 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. */ /* * Copyright (C) 2015 AppTik Project * * 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 io.apptik.comm.jus.okhttp3; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import io.apptik.comm.jus.Converter; import io.apptik.comm.jus.DefaultRetryPolicy; import io.apptik.comm.jus.Jus; import io.apptik.comm.jus.Marker; import io.apptik.comm.jus.NetworkRequest; import io.apptik.comm.jus.NetworkResponse; import io.apptik.comm.jus.ParseError; import io.apptik.comm.jus.Request; import io.apptik.comm.jus.RequestListener; import io.apptik.comm.jus.RequestQueue; import io.apptik.comm.jus.Response; import io.apptik.comm.jus.converter.Converters; import io.apptik.comm.jus.error.JusError; import io.apptik.comm.jus.http.HTTP; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.SocketPolicy; import static io.apptik.comm.jus.Request.Method.GET; import static io.apptik.comm.jus.Request.Method.HEAD; import static io.apptik.comm.jus.Request.Method.POST; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; public final class SimpleRequestTest { @Rule public final MockWebServer server = new MockWebServer(); public RequestQueue queue; public Service example = new Service(); class Service { Request<String> getString() { return new Request<>(GET, server.url("/").toString(), String.class) .prepRequestQueue(queue).setTag("str"); } Request<NetworkResponse> getHead() { return new Request<>(HEAD, server.url("/").toString(), NetworkResponse.class) .prepRequestQueue(queue); } Request<Number> getNumber() { return new Request<>(GET, server.url("/").toString(), Number.class) .prepRequestQueue(queue); } Request<NetworkResponse> getBody() { return new Request<>(GET, server.url("/").toString(), NetworkResponse.class) .prepRequestQueue (queue); } Request<String> postString(String body) throws IOException { return new Request<>(POST, server.url("/").toString(), String.class) .setRequestData(body, new Converters.StringRequestConverter()) .prepRequestQueue(queue); } Request<Number> postNumber(Number body, Converter converter) throws IOException { return new Request<>(POST, server.url("/").toString(), Number.class) .setRequestData(body, converter) .prepRequestQueue(queue); } } @Before public void setup() { queue = Jus.newRequestQueue(null, new OkHttpStack()); } @Test public void http200Sync() throws IOException, ExecutionException, InterruptedException { server.enqueue(new MockResponse().setBody("Hi")); Request<String> request = example.getString().enqueue(); request.getFuture().get(); Response<String> response = request.getRawResponse(); assertThat(response.isSuccess()).isTrue(); assertThat(response.result).isEqualTo("Hi"); assertThat(response.error).isNull(); } @Test public void httpHead200Sync() throws IOException, ExecutionException, InterruptedException { server.enqueue(new MockResponse().setBody("Hello")); Request<NetworkResponse> request = example.getHead().enqueue(); NetworkResponse networkResponse = request.getFuture().get(); Response<NetworkResponse> response = request.getRawResponse(); assertThat(response.isSuccess()).isTrue(); assertThat(response.error).isNull(); assertThat(networkResponse.data.length).isEqualTo(0); assertThat(networkResponse.headers.get(HTTP.CONTENT_LEN)).isEqualTo("5"); } @Test public void http200Async() throws InterruptedException, ExecutionException { server.enqueue(new MockResponse().setBody("Hi")); final AtomicReference<String> responseRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); Request<String> request = example.getString().addErrorListener(new RequestListener.ErrorListener () { @Override public void onError(JusError error) { error.printStackTrace(); } }).addResponseListener(new RequestListener.ResponseListener<String>() { @Override public void onResponse(String response) { responseRef.set(response); latch.countDown(); } }).enqueue(); assertTrue(latch.await(2, SECONDS)); Response<String> response = request.getRawResponse(); String response1 = responseRef.get(); assertThat(response.isSuccess()).isTrue(); assertThat(response.result).isEqualTo("Hi"); assertThat(response.error).isNull(); assertThat(response1).isEqualTo("Hi"); } @Test public void http404Sync() throws IOException, InterruptedException { server.enqueue(new MockResponse().setResponseCode(404).setBody("Hi")); Request<String> request = example.getString(); try { request.enqueue().getFuture().get(); } catch (ExecutionException e) { JusError error = (JusError) e.getCause(); assertThat(error).isNotNull(); assertThat(error.networkResponse.statusCode).isEqualTo(404); assertThat(error.networkResponse.getBodyAsString()).isEqualTo("Hi"); } Response<String> response = request.getRawResponse(); assertThat(response.isSuccess()).isFalse(); assertThat(response.result).isNull(); assertThat(response.error).isNotNull(); assertThat(response.error.networkResponse.statusCode).isEqualTo(404); assertThat(response.error.networkResponse.getBodyAsString()).isEqualTo("Hi"); } @Test public void http404Async() throws InterruptedException, IOException { server.enqueue(new MockResponse().setResponseCode(404).setBody("Hi")); final AtomicReference<String> responseRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); Request<String> request = example.getString() .addErrorListener(new RequestListener.ErrorListener() { @Override public void onError(JusError error) { responseRef.set(error.networkResponse.getBodyAsString()); latch.countDown(); } }) .addResponseListener(new RequestListener.ResponseListener<String>() { @Override public void onResponse(String response) { fail(); } }).enqueue(); assertTrue(latch.await(2, SECONDS)); String response1 = responseRef.get(); Response<String> response = request.getRawResponse(); assertThat(response.isSuccess()).isFalse(); assertThat(response.result).isNull(); assertThat(response.error).isNotNull(); assertThat(response.error.networkResponse.statusCode).isEqualTo(404); assertThat(response.error.networkResponse.getBodyAsString()).isEqualTo("Hi"); } @Test public void transportProblemSync() { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); Request<String> call = example.getString().setRetryPolicy(new DefaultRetryPolicy(1,0,1)).enqueue(); try { call.getFuture().get(); fail(); } catch (Exception ignored) { // Throwable failure = ignored.getCause(); // assertThat(failure).isInstanceOf(NoConnectionError.class); // assertThat(failure.getCause()).isInstanceOf(ConnectException.class); // assertThat(failure.getCause().getMessage()).isEqualTo("Connection refused"); Response<String> response = call.getRawResponse(); assertThat(response.isSuccess()).isFalse(); assertThat(response.result).isNull(); assertThat(response.error).isNotNull(); // assertThat(response.error).isInstanceOf(NoConnectionError.class); // assertThat(response.error.getCause()).isInstanceOf(ConnectException.class); // assertThat(failure.getCause().getMessage()).isEqualTo("Connection refused"); } } @Test public void transportProblemAsync() throws InterruptedException { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); final AtomicReference<Throwable> failureRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); Request<String> request = example.getString() .addErrorListener(new RequestListener.ErrorListener() { @Override public void onError(JusError error) { failureRef.set(error); latch.countDown(); } }) .addResponseListener(new RequestListener.ResponseListener<String>() { @Override public void onResponse(String response) { throw new AssertionError(); } }); request.enqueue(); //needs to wait more as the default retry policy is 1+2+4 assertTrue(latch.await(17, SECONDS)); Throwable failure = failureRef.get(); // assertThat(failure.getCause()).isInstanceOf(ConnectException.class); // assertThat(failure.getCause().getMessage()).isEqualTo("Connection refused"); Response<String> response = request.getRawResponse(); assertThat(response.isSuccess()).isFalse(); assertThat(response.result).isNull(); assertThat(response.error).isNotNull(); // assertThat(response.error).isInstanceOf(NoConnectionError.class); // assertThat(response.error.getCause()).isInstanceOf(ConnectException.class); // assertThat(failure.getCause().getMessage()).isEqualTo("Connection refused"); } @Test public void conversionProblemOutgoing() throws IOException, InterruptedException { try { example.postNumber(777, new Converter<Number, NetworkRequest>() { @Override public NetworkRequest convert(Number value) throws IOException { throw new UnsupportedOperationException("I am broken!"); } }).enqueue(); fail(); } catch (IOException e) { assertThat(e).hasCauseExactlyInstanceOf(UnsupportedOperationException.class); assertThat(e.getCause()).hasMessage("I am broken!"); } } @Test public void conversionProblemIncomingSync() throws IOException, InterruptedException { queue.addConverterFactory(new ToNumberConverterFactory() { @Override public Converter<NetworkResponse, ?> fromResponse(Type type, Annotation[] annotations) { return new Converter<NetworkResponse, Number>() { @Override public Number convert(NetworkResponse value) throws IOException { throw new UnsupportedOperationException("I am broken!"); } }; } }); server.enqueue(new MockResponse().setBody("Hi")); Request<Number> call = example.postNumber(777, new ToNumberConverterFactory().toRequest(Number.class, null)); try { call.enqueue().getFuture().get(); fail(); } catch (ExecutionException e) { assertThat(e.getCause()).isExactlyInstanceOf(ParseError.class); assertThat(e.getCause().getCause()) .isExactlyInstanceOf(UnsupportedOperationException.class) .hasMessage("I am broken!"); } } @Test public void conversionProblemIncomingConverterRuntimeException() throws IOException, InterruptedException { queue.addConverterFactory(new ToNumberConverterFactory() { @Override public Converter<NetworkResponse, ?> fromResponse(Type type, Annotation[] annotations) { return new Converter<NetworkResponse, Number>() { @Override public Number convert(NetworkResponse value) throws IOException { // Some serialization libraries mask transport problems in runtime // exceptions. Bad! throw new RuntimeException("wrapper", new IOException("cause")); } }; } }); server.enqueue(new MockResponse().setBody("777")); Request<Number> call = example.getNumber(); try { call.enqueue().getFuture().get(); fail(); } catch (ExecutionException e) { assertThat(e.getCause()).isExactlyInstanceOf(ParseError.class); assertThat(e.getCause().getCause()).isInstanceOf(RuntimeException.class); assertThat(e.getCause().getCause().getCause()).isInstanceOf(IOException.class); assertThat(e.getCause().getCause().getCause()).hasMessage("cause"); } } @Test public void conversionProblemIncomingAsync() throws InterruptedException, IOException { queue.addConverterFactory(new ToNumberConverterFactory() { @Override public Converter<NetworkResponse, ?> fromResponse(Type type, Annotation[] annotations) { return new Converter<NetworkResponse, Number>() { @Override public Number convert(NetworkResponse value) throws IOException { throw new UnsupportedOperationException("I am broken!"); } }; } }); server.enqueue(new MockResponse().setBody("777")); final AtomicReference<Throwable> failureRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); Request<Number> request = example.postNumber(777, new ToNumberConverterFactory().toRequest(Number.class, null)) .addErrorListener(new RequestListener.ErrorListener() { @Override public void onError(JusError error) { failureRef.set(error.getCause()); latch.countDown(); } }) .addResponseListener(new RequestListener.ResponseListener<Number>() { @Override public void onResponse(Number response) { throw new AssertionError(); } }).enqueue(); assertTrue(latch.await(2, SECONDS)); assertThat(failureRef.get()).isInstanceOf(UnsupportedOperationException.class) .hasMessage("I am broken!"); } /** * 10.2.5 204 No Content - means what it says, however representations of HTTP responses may * not only consider the entity-body(which anyway is not 'MUST' restricted) but * entity-headers as well */ @Test public void http204_DOES_NOT_SkipConverter() throws IOException, ExecutionException, InterruptedException { final Converter<NetworkResponse, Number> converter = spy(new Converter<NetworkResponse, Number>() { @Override public Number convert(NetworkResponse value) throws IOException { if (value.data == null || value.data.length == 0) { return null; } return Double.parseDouble(value.getBodyAsString()); } }); queue.addConverterFactory(new ToNumberConverterFactory() { @Override public Converter<NetworkResponse, ?> fromResponse(Type type, Annotation[] annotations) { return converter; } }); server.enqueue(new MockResponse().setStatus("HTTP/1.1 204 Nothin")); Request<Number> request = example.getNumber().enqueue(); request.getFuture().get(); Response<Number> response = request.getRawResponse(); assertThat(response.isSuccess()).isTrue(); assertThat(response.result).isNull(); verify(converter).convert((NetworkResponse) Mockito.anyObject()); verifyNoMoreInteractions(converter); } /** * 10.2.6 205 Reset Content - MUST NOT include an entity, however a converter may * anyway decide to use this(to generate Event to be emitted for example) */ @Test public void http205_DOES_NOT_SkipConverter() throws IOException, ExecutionException, InterruptedException { final Converter<NetworkResponse, Number> converter = spy(new Converter<NetworkResponse, Number>() { @Override public Number convert(NetworkResponse value) throws IOException { if (value == null || value.data == null || value.data.length == 0) { return null; } return Double.parseDouble(value.getBodyAsString()); } }); queue.addConverterFactory(new ToNumberConverterFactory() { @Override public Converter<NetworkResponse, ?> fromResponse(Type type, Annotation[] annotations) { return converter; } }); server.enqueue(new MockResponse().setStatus("HTTP/1.1 205 Nothin")); Request<Number> request = example.getNumber().enqueue(); request.getFuture().get(); Response<Number> response = request.getRawResponse(); assertThat(response.isSuccess()).isTrue(); assertThat(response.result).isNull(); verify(converter).convert((NetworkResponse) Mockito.anyObject()); verifyNoMoreInteractions(converter); } @Test public void successfulRequestResponseWhenMimeTypeMissing() throws Exception { server.enqueue(new MockResponse().setBody("Hi").removeHeader("Content-Type")); Request<String> request = example.getString().enqueue(); request.getFuture().get(); Response<String> response = request.getRawResponse(); assertThat(response.result).isEqualTo("Hi"); } @Test public void responseBody() throws IOException, ExecutionException, InterruptedException { server.enqueue(new MockResponse().setBody("1234")); NetworkResponse response = example.getBody().enqueue().getFuture().get(); assertThat(response.getBodyAsString()).isEqualTo("1234"); } @Test public void responseBodyBuffers() throws IOException, InterruptedException { server.enqueue(new MockResponse() .setBody("1234") .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY)); Request<NetworkResponse> buffered = example.getBody(); // When buffering we will detect all socket problems before returning the Response. try { buffered.enqueue().getFuture().get(); fail(); } catch (ExecutionException e) { //we have a protocol error from OkHttp as we read the whole body when we got it assertThat(e.getCause()).hasMessage("Response Body not completely received"); assertThat(buffered.getRawResponse().error.networkResponse).isNotNull(); } } @Test public void emptyResponse() throws IOException, ExecutionException, InterruptedException { server.enqueue(new MockResponse().setBody("").addHeader("Content-Type", "text/stringy")); Request<String> request = example.getString().enqueue(); request.getFuture().get(); Response<String> response = request.getRawResponse(); assertThat(response.result).isEqualTo(""); } @Test public void cancelBeforeEnqueue() { Request<String> call = example.getString(); call.cancel(); try { call.enqueue(); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessage("Canceled"); } } @Test public void cloningExecutedRequestDoesNotCopyState() throws IOException, ExecutionException, InterruptedException { server.enqueue(new MockResponse().setBody("Hi")); server.enqueue(new MockResponse().setBody("Hello")); Request<String> call = example.getString().enqueue(); assertThat(call.getFuture().get()).isEqualTo("Hi"); Request<String> cloned = call.clone(); assertThat(cloned.enqueue().getFuture().get()).isEqualTo("Hello"); } @Test public void cancelRequest() throws InterruptedException { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)); Request<String> call = example.getString(); final AtomicReference<Marker> markerRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); call .addErrorListener(new RequestListener.ErrorListener() { @Override public void onError(JusError error) { throw new AssertionError(); } }) .addResponseListener(new RequestListener.ResponseListener<String>() { @Override public void onResponse(String response) { throw new AssertionError(); } }) .addMarkerListener(new RequestListener.MarkerListener() { @Override public void onMarker(Marker marker, Object... args) { if (Request.EVENT_CACHE_DISCARD_CANCELED.equals(marker.name) || Request.EVENT_NETWORK_DISCARD_CANCELED.equals(marker.name) || Request.EVENT_CANCELED_AT_DELIVERY.equals(marker.name)) { markerRef.set(marker); latch.countDown(); } } }); call.enqueue().cancel(); assertTrue(latch.await(2, SECONDS)); assertThat(markerRef.get().name).containsIgnoringCase("canceled"); } @Test public void cancelRequestWhenInRetry() throws InterruptedException, ExecutionException { server.enqueue(new MockResponse().setResponseCode(200).setBody("#whatcanyoudo") .setBodyDelay(5101, TimeUnit.MILLISECONDS)); final CountDownLatch latch = new CountDownLatch(2); example.getString().addMarkerListener(new RequestListener.MarkerListener() { @Override public void onMarker(Marker marker, Object... args) { if (Request.EVENT_NETWORK_RETRY.equals(marker.name)) { queue.cancelAll("str"); latch.countDown(); } else if (Request.EVENT_NETWORK_RETRY_FAILED.equals(marker.name)) { throw new AssertionError(); } else if (Request.EVENT_NETWORK_DISCARD_CANCELED.equals(marker.name)) { latch.countDown(); } } }).addErrorListener(new RequestListener.ErrorListener() { @Override public void onError(JusError error) { throw new AssertionError(); } }).enqueue(); assertTrue(latch.await(8, SECONDS)); } @After public void after() { queue.stopWhenDone(); } }