com.xing.api.CallSpecTest.java Source code

Java tutorial

Introduction

Here is the source code for com.xing.api.CallSpecTest.java

Source

/*
 * Copyright () 2015 XING AG (http://xing.com/)
 *
 * 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 com.xing.api;

import com.squareup.okhttp.Call;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
import com.xing.api.CallSpec.ExceptionCatchingRequestBody;
import com.xing.api.HttpError.Error.Reason;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import okio.Buffer;
import okio.BufferedSource;
import rx.observables.BlockingObservable;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.junit.Assert.assertNotNull;

/**
 * @author serj.lotutovici
 */
@SuppressWarnings({ "MagicNumber", "ConstantConditions" })
public class CallSpecTest {
    @Rule
    public final MockWebServer server = new MockWebServer();
    public XingApi mockApi;
    public HttpUrl httpUrl;

    @Before
    public void setUp() throws Exception {
        httpUrl = server.url("/");
        mockApi = new XingApi.Builder().apiEndpoint(httpUrl).loggedOut().build();
    }

    @After
    public void tearDown() throws Exception {
        mockApi = null;
    }

    @Test
    public void builderFailsWithoutSpec() throws Exception {
        try {
            builder(HttpMethod.DELETE, "", false).request();
            fail("#request() should fail");
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("#request() can be called only after #build()");
        }
    }

    @Test
    public void builderAcceptsPathParams() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "/{param1}/{param2}", false).responseAs(Object.class)
                .pathParam("param1", "test1").pathParam("param2", "test2");
        builder.build();

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.GET.method());
        assertThat(request.urlString()).isEqualTo(httpUrl + "test1/test2");
        assertThat(request.body()).isNull();
    }

    @Test
    public void builderAcceptsPathParamsAsVarList() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "/{params}", false).responseAs(Object.class)
                .pathParam("params", "one", "two", "three");
        builder.build();

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.GET.method());
        assertThat(request.urlString()).isEqualTo(httpUrl + "one,two,three");
        assertThat(request.body()).isNull();
    }

    @Test
    public void builderAcceptsPathParamsAsList() throws Exception {
        List<String> params = new ArrayList<>(3);
        params.add("one");
        params.add("two");
        params.add("three");

        CallSpec.Builder builder = builder(HttpMethod.GET, "/{params}", false).responseAs(Object.class)
                .pathParam("params", params);
        builder.build();

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.GET.method());
        assertThat(request.urlString()).isEqualTo(httpUrl + "one,two,three");
        assertThat(request.body()).isNull();
    }

    @Test
    public void builderAcceptsHeaders() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "/", false).responseAs(Object.class)
                .header("Test1", "hello").header("Test2", "hm");
        builder.build();

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.GET.method());
        assertThat(request.headers().names()).contains("Test1").contains("Test2");
        assertThat(request.headers().values("Test1")).isNotEmpty().hasSize(1).contains("hello");
        assertThat(request.headers().values("Test2")).isNotEmpty().hasSize(1).contains("hm");
    }

    @Test
    public void builderFailsOnMalformedParams() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "/{%sdf}/", false);
        try {
            builder.pathParam("%sdf", "any");
            fail("Builder should fail on malformed params.");
        } catch (Exception e) {
            assertThat(e).isInstanceOf(IllegalArgumentException.class);
            assertThat(e).hasMessageContaining("Path parameter name must match")
                    .hasMessageEndingWith("Found: %sdf");
        }
    }

    @Test
    public void builderFailsIfPathParamNotExpected() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "/{test1}/", false);
        try {
            builder.pathParam("test2", "first");
            fail("Builder should fail on non expected params.");
        } catch (Exception e) {
            assertThat(e).isInstanceOf(IllegalArgumentException.class)
                    .hasMessageStartingWith("Resource path \"/{test1}/\" does not contain \"{test2}\".")
                    .hasMessageEndingWith("Or the path parameter has been already set.");
        }
    }

    @Test
    public void builderFailsIfPathParamsNotSet() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "/{test2}", false);
        try {
            builder.build();
            fail("Builder should fail if path params not set.");
        } catch (Exception e) {
            assertThat(e).isInstanceOf(IllegalStateException.class)
                    .hasMessage("Not all path params where set. Found 1 unsatisfied parameter(s)");
        }
    }

    @Test
    public void builderFailsOnAddingPathParamsAfterQueryParams() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "/{test1}", false);
        builder.queryParam("q", "test");
        try {
            builder.pathParam("test1", "test");
        } catch (Exception e) {
            assertThat(e).isInstanceOf(IllegalStateException.class)
                    .hasMessage("Path params must be set before query params.");
        }
    }

    @Test
    public void builderAttachesQueryParams() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "", false).responseAs(Object.class).queryParam("q",
                "test1");
        // Build the CallSpec so that we don't test this behaviour twice.
        builder.build().queryParam("w", "test2");

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.GET.method());
        assertThat(request.urlString()).isEqualTo(httpUrl + "?q=test1&w=test2");
        assertThat(request.body()).isNull();
    }

    @Test
    public void builderAttachesQueryParamsAsVarList() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.GET, "", false).responseAs(Object.class).queryParam("q",
                "testL", "testL");
        // Build the CallSpec so that we don't test this behaviour twice.
        builder.build().queryParam("w", "testL", "testL");

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.GET.method());
        assertThat(request.urlString()).isEqualTo(httpUrl + "?q=testL%2C%20testL&w=testL%2C%20testL");
        assertThat(request.body()).isNull();
    }

    @Test
    public void builderAttachesQueryParamsAsList() throws Exception {
        List<String> query = new ArrayList<>(2);
        query.add("testL");
        query.add("testL");

        CallSpec.Builder builder = builder(HttpMethod.GET, "", false).responseAs(Object.class).queryParam("q",
                query);
        // Build the CallSpec so that we don't test this behaviour twice.
        builder.build().queryParam("w", query);

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.GET.method());
        assertThat(request.urlString()).isEqualTo(httpUrl + "?q=testL%2C%20testL&w=testL%2C%20testL");
        assertThat(request.body()).isNull();
    }

    @Test
    public void builderAttachesFormFields() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.PUT, "", true).responseAs(Object.class).formField("f",
                "true");
        // Build the CallSpec so that we don't test this behaviour twice.
        builder.build().formField("e", "false");

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.PUT.method());
        assertThat(request.httpUrl()).isEqualTo(httpUrl);
        assertThat(request.body()).isNotNull();

        RequestBody body = request.body();
        assertThat(body.contentType()).isEqualTo(MediaType.parse("application/x-www-form-urlencoded"));

        Buffer buffer = new Buffer();
        body.writeTo(buffer);
        assertThat(buffer.readUtf8()).isEqualTo("f=true&e=false");
    }

    @Test
    public void builderAttachesFormFieldsAsLists() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.PUT, "", true).responseAs(Object.class).formField("f",
                "test1", "test2");
        // Build the CallSpec so that we don't test this behaviour twice.
        List<String> field = new ArrayList<>(2);
        field.add("test3");
        field.add("test4");
        builder.build().formField("e", field).formField("d", "test5", "test6");

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.PUT.method());
        assertThat(request.httpUrl()).isEqualTo(httpUrl);
        assertThat(request.body()).isNotNull();

        RequestBody body = request.body();
        assertThat(body.contentType()).isEqualTo(MediaType.parse("application/x-www-form-urlencoded"));

        Buffer buffer = new Buffer();
        body.writeTo(buffer);
        assertThat(buffer.readUtf8()).isEqualTo("f=test1%2C%20test2&e=test3%2C%20test4&d=test5%2C%20test6");
    }

    @Test
    public void builderEnsuresEmptyBody() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.PUT, "", false).responseAs(Object.class);
        // Build the CallSpec so that we can build the request.
        builder.build();

        Request request = builder.request();
        assertThat(request.method()).isEqualTo(HttpMethod.PUT.method());
        assertThat(request.httpUrl()).isEqualTo(httpUrl);
        assertThat(request.body()).isNotNull();

        RequestBody body = request.body();
        assertThat(body.contentType()).isNull();
        assertThat(body.contentLength()).isEqualTo(0);

        Buffer buffer = new Buffer();
        body.writeTo(buffer);
        assertThat(buffer.readUtf8()).isEmpty();
    }

    @Test
    public void builderAllowsRawBody() throws Exception {
        CallSpec.Builder builder = builder(HttpMethod.PUT, "", false).responseAs(Object.class)
                .body(RequestBody.create(MediaType.parse("application/text"), "Hey!"));
        // Build the CallSpec so that we can build the request.
        builder.build();

        Request request = builder.request();
        RequestBody body = request.body();
        assertThat(body.contentLength()).isEqualTo(4);
        assertThat(body.contentType().subtype()).isEqualTo("text");

        Buffer buffer = new Buffer();
        body.writeTo(buffer);
        assertThat(buffer.readUtf8()).isEqualTo("Hey!");
    }

    @Test
    public void builderAndSpecAllowJsonBody() throws Exception {
        TestMsg expectedBody1 = new TestMsg("Hey!", 42);
        CallSpec.Builder builder1 = builder(HttpMethod.PUT, "", false).responseAs(Object.class).body(TestMsg.class,
                expectedBody1);
        // Build the CallSpec so that we can build the request.
        builder1.build();
        assertRequestHasBody(builder1.request(), expectedBody1, 24);

        TestMsg expectedBody2 = new TestMsg("Fallout 4", 111);
        CallSpec.Builder builder2 = builder(HttpMethod.PUT, "", false).responseAs(Object.class);
        // Build the CallSpec so that we can build the request.
        CallSpec spec = builder2.build();
        spec.body(TestMsg.class, expectedBody2);
        assertRequestHasBody(builder2.request(), expectedBody2, 30);
    }

    @Test
    public void specThrowsIfCanceled() throws Exception {
        CallSpec spec = builder(HttpMethod.GET, "", false).responseAs(Object.class).build();
        spec.cancel();
        assertThat(spec.isCanceled()).isTrue();

        try {
            spec.execute();
            fail("#execute() should throw");
        } catch (IOException ex) {
            assertThat(ex.getMessage()).isNotEmpty().isEqualTo("Canceled");
        }

        // We need to rebuild the call, since #execute() will mark the call as executed.
        spec = builder(HttpMethod.GET, "", false).responseAs(Object.class).build();
        spec.cancel();

        final AtomicReference<Throwable> responseRef = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);
        //noinspection unchecked
        spec.enqueue(new Callback() {
            @Override
            public void onResponse(Response response) {

            }

            @Override
            public void onFailure(Throwable t) {
                responseRef.set(t);
                latch.countDown();
            }
        });

        assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue();

        Throwable th = responseRef.get();
        assertThat(th).isInstanceOf(IOException.class);
        assertThat(th.getMessage()).isNotEmpty().isEqualTo("Canceled");
    }

    @Test
    public void specThrowsIfExecutedTwice() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(204));

        CallSpec spec = builder(HttpMethod.DELETE, "", false).responseAs(Object.class).build();
        Response response = spec.execute();

        assertThat(spec.isExecuted()).isTrue();
        assertThat(response.code()).isEqualTo(204);
        assertThat(response.body()).isNull();
        assertThat(response.raw()).isNotNull();

        try {
            spec.execute();
            fail("#execute() should throw");
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isNotEmpty().isEqualTo("Call already executed");
        }

        try {
            //noinspection unchecked,ConstantConditions
            spec.enqueue(null);
            fail("#enqueue() should throw");
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isNotEmpty().isEqualTo("Call already executed");
        }
    }

    @Test
    public void specHandlesSuccessResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(200)
                .setBody("{\n" + "  \"msg\": \"success\",\n" + "  \"code\": 42\n" + '}'));

        CallSpec<TestMsg, Object> spec = this.<TestMsg, Object>builder(HttpMethod.GET, "/", false)
                .responseAs(TestMsg.class).build();

        Response<TestMsg, Object> response = spec.execute();
        assertSuccessResponse(response, new TestMsg("success", 42));
    }

    @Test
    public void specHandlesEmptyBodyResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(204));
        CallSpec spec = builder(HttpMethod.GET, "/", false).responseAs(Object.class).build();
        assertNoBodySuccessResponse(spec.execute(), 204);

        server.enqueue(new MockResponse().setResponseCode(205));
        spec = builder(HttpMethod.GET, "/", false).responseAs(Object.class).build();
        assertNoBodySuccessResponse(spec.execute(), 205);
    }

    @Test
    public void specHandlesVoidAsEmptyBodyResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(200).setBody("Test"));
        CallSpec<Void, Object> spec = this.<Void, Object>builder(HttpMethod.GET, "/", false).responseAs(Void.class)
                .build();
        assertNoBodySuccessResponse(spec.execute(), 200);

        server.enqueue(new MockResponse().setResponseCode(200).setBody("Test"));
        assertNoBodySuccessResponseAsync(spec.clone(), 200);
    }

    @Test
    public void specHandlesErrorResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(300)
                .setBody("{\n" + "  \"msg\": \"error\",\n" + "  \"code\": 13\n" + '}'));

        CallSpec<Object, TestMsg> spec = this.<Object, TestMsg>builder(HttpMethod.GET, "/", false)
                .responseAs(Object.class).errorAs(TestMsg.class).build();

        Response<Object, TestMsg> response = spec.execute();
        assertErrorResponse(response, new TestMsg("error", 13), 300);
    }

    @Test
    public void specHandlesSuccessResponseAsync() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(200)
                .setBody("{\n" + "  \"msg\": \"success\",\n" + "  \"code\": 42\n" + '}'));

        CallSpec<TestMsg, Object> spec = this.<TestMsg, Object>builder(HttpMethod.GET, "/", false)
                .responseAs(TestMsg.class).build();

        final AtomicReference<Response<TestMsg, Object>> responseRef = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);
        spec.enqueue(new Callback<TestMsg, Object>() {
            @Override
            public void onResponse(Response<TestMsg, Object> response) {
                responseRef.set(response);
                latch.countDown();
            }

            @Override
            public void onFailure(Throwable t) {
                fail("unexpected #onFailure() call");
            }
        });

        latch.await(2, TimeUnit.SECONDS);
        assertSuccessResponse(responseRef.get(), new TestMsg("success", 42));
    }

    @Test
    public void specHandlesEmptyBodyResponseAsync() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(204));
        CallSpec spec = builder(HttpMethod.GET, "/", false).responseAs(Object.class).build();
        assertNoBodySuccessResponseAsync(spec, 204);

        server.enqueue(new MockResponse().setResponseCode(205));
        spec = builder(HttpMethod.GET, "/", false).responseAs(Object.class).build();
        assertNoBodySuccessResponseAsync(spec, 205);
    }

    @SuppressWarnings("unchecked")
    @Test
    public void specHandlesRequestBuildFailure() throws Exception {
        XingApi failingApi = new XingApi.Builder().loggedOut().apiEndpoint(httpUrl).client(new OkHttpClient() {
            @Override
            public Call newCall(Request request) {
                throw new UnsupportedOperationException("I'm broken!");
            }
        }).build();

        CallSpec spec = new CallSpec.Builder(failingApi, HttpMethod.GET, "/", false).responseAs(Object.class)
                .build();

        final AtomicReference<Throwable> responseRef = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);
        spec.enqueue(new Callback<Object, Object>() {
            @Override
            public void onResponse(Response<Object, Object> response) {
                fail("unexpected #onSuccess() call");
            }

            @Override
            public void onFailure(Throwable t) {
                responseRef.set(t);
                latch.countDown();
            }
        });

        latch.await(2, TimeUnit.SECONDS);
        assertThat(responseRef.get()).isInstanceOf(UnsupportedOperationException.class).hasMessage("I'm broken!");
    }

    @Test
    public void specHandlesErrorResponseAsync() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(189)
                .setBody("{\n" + "  \"msg\": \"error\",\n" + "  \"code\": 14\n" + '}'));

        CallSpec<Object, TestMsg> spec = this.<Object, TestMsg>builder(HttpMethod.GET, "/", false)
                .responseAs(Object.class).errorAs(TestMsg.class).build();

        final AtomicReference<Response<Object, TestMsg>> responseRef = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);
        spec.enqueue(new Callback<Object, TestMsg>() {
            @Override
            public void onResponse(Response<Object, TestMsg> response) {
                responseRef.set(response);
                latch.countDown();
            }

            @Override
            public void onFailure(Throwable t) {
                fail("unexpected #onFailure() call");
            }
        });

        latch.await(2, TimeUnit.SECONDS);
        assertErrorResponse(responseRef.get(), new TestMsg("error", 14), 189);
    }

    @Test
    public void specHandlesIOExceptionAsFailureAsync() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(200).setBody("\\}"));

        CallSpec<TestMsg, Object> spec = this.<TestMsg, Object>builder(HttpMethod.GET, "/", false)
                .responseAs(TestMsg.class).build();

        final AtomicReference<Throwable> responseRef = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);
        spec.enqueue(new Callback<TestMsg, Object>() {
            @Override
            public void onResponse(Response<TestMsg, Object> response) {
                fail("unexpected #onResponse() call");
            }

            @Override
            public void onFailure(Throwable t) {
                responseRef.set(t);
                latch.countDown();
            }
        });

        latch.await(2, TimeUnit.SECONDS);
        Throwable t = responseRef.get();
        assertThat(t).isInstanceOf(IOException.class);
        assertThat(t.getMessage()).contains("malformed JSON");
    }

    @Test
    public void exceptionCatchingBodyThrows() throws Exception {
        ResponseBody throwingBody = new ResponseBody() {
            @Override
            public MediaType contentType() {
                //noinspection ConstantConditions
                return MediaType.parse("application/h");
            }

            @Override
            public long contentLength() throws IOException {
                throw new IOException("Broken body!");
            }

            @Override
            public BufferedSource source() throws IOException {
                throw new IOException("Broken body!");
            }
        };

        // Test content length throws
        ExceptionCatchingRequestBody body = new ExceptionCatchingRequestBody(throwingBody);
        assertThat(body.contentType()).isEqualTo(throwingBody.contentType());
        try {
            body.contentLength();
        } catch (IOException ignored) {
        }
        try {
            body.throwIfCaught();
        } catch (IOException e) {
            assertThat(e.getMessage()).isEqualTo("Broken body!");
        }

        // Test source throws, here we need new object
        body = new ExceptionCatchingRequestBody(throwingBody);
        assertThat(body.contentType()).isEqualTo(throwingBody.contentType());
        try {
            body.source();
        } catch (IOException ignored) {
        }
        try {
            body.throwIfCaught();
        } catch (IOException e) {
            assertThat(e.getMessage()).isEqualTo("Broken body!");
        }
    }

    @Test
    public void specRawStreamsSuccessResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(200)
                .setBody("{\n" + "  \"msg\": \"success\",\n" + "  \"code\": 200\n" + '}'));

        CallSpec<TestMsg, Object> spec = this.<TestMsg, Object>builder(HttpMethod.GET, "/", false)
                .responseAs(TestMsg.class).build();

        BlockingObservable<Response<TestMsg, Object>> blocking = spec.rawStream().toBlocking();
        Response<TestMsg, Object> response = blocking.first();

        assertNotNull(response.body());
        assertThat(response.body().code).isEqualTo(200);
        assertThat(response.body().msg).isEqualTo("success");
    }

    @Test
    public void specRawStreamsErrorResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(405)
                .setBody("{\n" + "  \"error_name\": \"TEST_ERROR\",\n" + "  \"message\": \"Terrible Error.\",\n"
                        + "  \"errors\": [\n" + "    {\n" + "      \"field\": \"no_field\",\n"
                        + "      \"reason\": \"UNEXPECTED\"\n" + "    }\n" + "  ]\n" + '}'));

        CallSpec<Object, HttpError> spec = this.<Object, HttpError>builder(HttpMethod.GET, "/", false)
                .responseAs(Object.class).build();

        BlockingObservable<Response<Object, HttpError>> blocking = spec.rawStream().toBlocking();
        Response<Object, HttpError> response = blocking.first();

        assertThat(response.isSuccess()).isFalse();
        assertNotNull(response.error());
        assertThat(response.error().message()).isEqualTo("Terrible Error.");
        assertThat(response.error().name()).isEqualTo("TEST_ERROR");
        assertThat(response.error().errors().get(0)).isEqualTo(new HttpError.Error("no_field", Reason.UNEXPECTED));
    }

    @Test
    public void specRawStreamsError() throws Exception {
        server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));

        CallSpec<Object, Object> spec = builder(HttpMethod.GET, "/", false).responseAs(Object.class).build();

        BlockingObservable<Response<Object, Object>> blocking = spec.rawStream().toBlocking();
        try {
            blocking.first();
            fail("This should throw an error.");
        } catch (RuntimeException t) {
            assertThat(t.getCause()).isInstanceOf(IOException.class);
        }
    }

    @Test
    public void specStreamsSuccessResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(200)
                .setBody("{\n" + "  \"msg\": \"Hey!\",\n" + "  \"code\": 42\n" + '}'));

        CallSpec<TestMsg, Object> spec = this.<TestMsg, Object>builder(HttpMethod.GET, "/", false)
                .responseAs(TestMsg.class).build();

        BlockingObservable<TestMsg> blocking = spec.stream().toBlocking();
        TestMsg result = blocking.first();

        assertThat(result.msg).isEqualTo("Hey!");
        assertThat(result.code).isEqualTo(42);
    }

    @Test
    public void specStreamsErrorResponse() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(405)
                .setBody("{\n" + "  \"error_name\": \"TEST_ERROR2\",\n" + "  \"message\": \"Yet another error.\",\n"
                        + "  \"errors\": [\n" + "    {\n" + "      \"field\": \"some_field\",\n"
                        + "      \"reason\": \"FIELD_DEPRECATED\"\n" + "    }\n" + "  ]\n" + '}'));

        CallSpec<Object, Object> spec = builder(HttpMethod.DELETE, "/", false).responseAs(Object.class).build();

        BlockingObservable<Object> blocking = spec.stream().toBlocking();
        try {
            blocking.first();
            fail("This should throw an error.");
        } catch (Throwable t) {
            assertThat(t.getCause()).isInstanceOf(HttpException.class);

            HttpException exception = (HttpException) t.getCause();
            assertThat(exception.code()).isEqualTo(405);
            assertThat(exception.message()).isEqualTo("OK");

            HttpError error = (HttpError) exception.error();
            assertThat(error.name()).isEqualTo("TEST_ERROR2");
            assertThat(error.message()).isEqualTo("Yet another error.");
            assertThat(error.errors().get(0)).isEqualTo(new HttpError.Error("some_field", Reason.FIELD_DEPRECATED));
        }
    }

    @Test
    public void specStreamsError() throws Exception {
        server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));

        CallSpec<Object, Object> spec = builder(HttpMethod.PUT, "/", false).responseAs(Object.class).build();

        BlockingObservable<Object> blocking = spec.stream().toBlocking();
        try {
            blocking.first();
            fail("Observable should throw.");
        } catch (Throwable t) {
            assertThat(t.getCause()).isInstanceOf(IOException.class);
        }
    }

    private static void assertSuccessResponse(Response<TestMsg, Object> response, TestMsg expected) {
        assertThat(response.code()).isEqualTo(200);
        assertThat(response.error()).isNull();
        assertThat(response.message()).isEqualTo("OK");
        assertThat(response.headers()).isNotNull();

        TestMsg msg = response.body();
        assertNotNull(msg);
        assertThat(msg.msg).isEqualTo(expected.msg);
        assertThat(msg.code).isEqualTo(expected.code);
    }

    private static void assertErrorResponse(Response<Object, TestMsg> response, TestMsg expected, int code) {
        assertThat(response.isSuccess()).isFalse();
        assertThat(response.code()).isEqualTo(code);
        assertThat(response.body()).isNull();

        TestMsg body = response.error();
        assertNotNull(body);
        assertThat(body.msg).isEqualTo(expected.msg);
        assertThat(body.code).isEqualTo(expected.code);
    }

    private static void assertRequestHasBody(Request request, TestMsg expected, int contentLength)
            throws IOException {
        RequestBody body = request.body();
        assertThat(body.contentLength()).isEqualTo(contentLength);
        assertThat(body.contentType().subtype()).isEqualTo("json");

        Buffer buffer = new Buffer();
        body.writeTo(buffer);
        assertThat(buffer.readUtf8()).contains("\"msg\":\"" + expected.msg + '"')
                .contains("\"code\":" + expected.code).startsWith("{").endsWith("}").hasSize(contentLength);
    }

    private static void assertNoBodySuccessResponse(Response response, int code) throws IOException {
        assertThat(response.code()).isEqualTo(code);
        assertThat(response.error()).isNull();
        assertThat(response.body()).isNull();
        assertThat(response.isSuccess()).isTrue();

        ResponseBody body = response.raw().body();
        assertThat(body).isNotNull();
        // User may want to ignore the content of another response.
        if (code == 204 || code == 205)
            assertThat(body.contentLength()).isEqualTo(0L);
        assertThat(body.contentType()).isNull();

        try {
            body.source();
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("Cannot read raw response body of a parsed body.");
        }
    }

    private static void assertNoBodySuccessResponseAsync(CallSpec spec, int code) throws Exception {
        final AtomicReference<Response<Object, Object>> responseRef = new AtomicReference<>();
        final CountDownLatch latch = new CountDownLatch(1);
        //noinspection unchecked
        spec.enqueue(new Callback<Object, Object>() {
            @Override
            public void onResponse(Response<Object, Object> response) {
                responseRef.set(response);
                latch.countDown();
            }

            @Override
            public void onFailure(Throwable t) {
                fail("unexpected #onFailure() call");
            }
        });
        latch.await(2, TimeUnit.SECONDS);
        assertNoBodySuccessResponse(responseRef.get(), code);
    }

    private <RT, ET> CallSpec.Builder<RT, ET> builder(HttpMethod httpMethod, String path, boolean formEncoded) {
        return new CallSpec.Builder<>(mockApi, httpMethod, path, formEncoded);
    }

    static final class TestMsg {
        final String msg;
        final int code;

        public TestMsg(String msg, int code) {
            this.msg = msg;
            this.code = code;
        }
    }
}