All Downloads are FREE. Search and download functionalities are using the official Maven repository.

test.java.com.cloudant.tests.ViewsTest Maven / Gradle / Ivy

There is a newer version: 2.20.1
Show newest version
/*
 * Copyright © 2015, 2018 IBM Corp. All rights reserved.
 * Copyright (C) 2011 lightcouch.org
 *
 * 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.cloudant.tests;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import com.cloudant.client.api.model.Document;
import com.cloudant.client.api.model.Response;
import com.cloudant.client.api.views.AllDocsResponse;
import com.cloudant.client.api.views.Key;
import com.cloudant.client.api.views.ViewMultipleRequest;
import com.cloudant.client.api.views.ViewRequest;
import com.cloudant.client.api.views.ViewRequestBuilder;
import com.cloudant.client.api.views.ViewResponse;
import com.cloudant.http.HttpConnectionInterceptorContext;
import com.cloudant.test.main.RequiresCloudant;
import com.cloudant.test.main.RequiresDB;
import com.cloudant.tests.base.TestWithDbPerTest;
import com.cloudant.tests.extensions.CloudantClientExtension;
import com.cloudant.tests.extensions.DatabaseExtension;
import com.cloudant.tests.extensions.MultiExtension;
import com.cloudant.tests.util.ContextCollectingInterceptor;
import com.cloudant.tests.util.Utils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.function.Executable;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

@RequiresDB
public class ViewsTest extends TestWithDbPerTest {

    public static ContextCollectingInterceptor cci = new ContextCollectingInterceptor();
    public static CloudantClientExtension interceptedClient = new CloudantClientExtension
            (CloudantClientHelper.getClientBuilder()
            .interceptors(cci));
    public static DatabaseExtension.PerClass interceptedDB = new DatabaseExtension.PerClass
            (interceptedClient);

    @RegisterExtension
    public static MultiExtension extensions = new MultiExtension(
            interceptedClient,
            interceptedDB
    );

    @BeforeEach
    public void beforeEach() throws Exception {
        Utils.putDesignDocs(db);
    }

    @Test
    public void queryView() throws Exception {
        init();
        List foos = db.getViewRequestBuilder("example", "foo").newRequest(Key.Type.STRING,
                Object.class).includeDocs(true).build().getResponse().getDocsAs(Foo.class);
        assertThat(foos.size(), not(0));
    }

    @Test
    public void byKey() throws Exception {
        init();
        List foos = db.getViewRequestBuilder("example", "foo").newRequest(Key.Type.STRING,
                Object.class).includeDocs(true).keys("key-1").build().getResponse().getDocsAs(Foo
                .class);
        assertThat(foos.size(), is(1));
    }

    @Test
    public void byKeys() throws Exception {
        init();
        List foos = db.getViewRequestBuilder("example", "foo").newRequest(Key.Type.STRING,
                Object.class).includeDocs(true).keys("key-1", "key-2").build().getResponse()
                .getDocsAs(Foo.class);
        assertThat(foos.size(), is(2));
    }

    @Test
    public void byKeysIncludeDocsFalse() throws Exception {
        init();
        // attempting to call getDocs() for a view with include_docs=false raises the exception
        // java.lang.IllegalStateException: Cannot getDocs() when include_docs is false.
        assertThrows(IllegalStateException.class, () -> {
            db.getViewRequestBuilder("example", "foo").newRequest(Key.Type
                            .STRING,
                    Object.class).includeDocs(false).keys("key-1", "key-2").build()
                    .getResponse().getDocs();
        });
    }

    @Test
    public void byNonExistentAndExistingKey() throws Exception {
        init();
        List> foos = db.getViewRequestBuilder("example", "foo")
                .newRequest(Key.Type.STRING, Object.class).includeDocs(true).keys("key-1",
                        "non-existent")
                .build().getResponse().getRows();
        assertThat(foos.size(), is(1));
        for (ViewResponse.Row row : foos) {
            if (row.getError() == null) {
                assertThat(row.getKey().toString(), is("key-1"));
            } else {
                assertNotNull(row.getDocument());
            }
        }
    }

    @Test
    public void byStartAndEndKey() throws Exception {
        init();
        List foos = db.getViewRequestBuilder("example", "foo").newRequest(Key.Type.STRING,
                Object.class).includeDocs(true).startKey("key-1").endKey("key-2").build()
                .getResponse().getDocsAs(Foo.class);
        assertThat(foos.size(), is(2));
    }

    /**
     * Assert that passing a boolean key with value 'false'
     * in query will produce a result list of
     * all false docs.
     */
    @Test
    public void queryWithStartAndEndBooleanKey() throws Exception {
        init();
        List result = db.getViewRequestBuilder("example", "boolean").newRequest(Key.Type
                .BOOLEAN, Object.class).startKey(false).endKey(false).build()
                .getResponse().getValues();
        assertThat(result.size(), is(1));
    }

    /**
     * Assert that passing a boolean key with value 'true' in
     * queryView will produce a result list of a true doc.
     */
    @Test
    public void queryPageWithStartAndEndBooleanKey() throws Exception {
        init();
        ViewResponse result = db.getViewRequestBuilder("example", "boolean")
                .newPaginatedRequest(Key.Type.BOOLEAN, Object.class).startKey(true).endKey(true)
                .rowsPerPage(2).build()
                .getResponse();

        List resultList = result.getValues();

        assertThat(resultList.size(), is(2));
    }

    /**
     * Assert that passing a string key with double quotes
     * or spaces in query and queryView will produce
     * a result list without exception.
     */
    @Test
    public void queryWithStartAndEndStringIntKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "spaces_created").newRequest
                (Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex(" spaces ").add(1))
                .endKey(Key.complex(" spaces 1").add(2000))
                .build().getResponse().getValues();

        assertThat(result.size(), is(2));
    }

    /**
     * Assert that passing a string key with spaces
     * in queryPage will produce a result list
     * without exception.
     */
    @Test
    public void queryPageWithStartAndEndStringIntKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "spaces_created")
                .newPaginatedRequest(Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex(" spaces ").add(1))
                .endKey(Key.complex(" spaces 0").add(2000))
                .rowsPerPage(30).build().getResponse().getValues();

        assertThat(result.size(), is(1));
    }

    /**
     * Assert that passing a string key with quotes
     * in query will produce a result list
     * without exception.
     */
    @Test
    public void queryWithStartAndEndStringWithQuotesAndIntKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "quotes_created").newRequest
                (Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex("\"quotes\" ").add(1))
                .endKey(Key.complex("\"quotes\" 0").add(2000))
                .build().getResponse().getValues();

        assertThat(result.size(), is(1));
    }

    /**
     * Assert that passing a string key with quotes
     * in query will produce a result list
     * without exception.
     */
    @Test
    public void queryPageWithStartAndEndStringWithQuotesAndIntKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "quotes_created")
                .newPaginatedRequest(Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex("\"quotes\" ").add(1))
                .endKey(Key.complex("\"quotes\" 1").add(2000))
                .rowsPerPage(30).build().getResponse().getValues();

        assertThat(result.size(), is(2));
    }

    /**
     * Assert that passing an array of integers in query
     * will produce a result list of all docs.
     */
    @Test
    public void queryWithStartAndEndIntKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "created").newRequest
                (Key.Type.COMPLEX, Object.class).startKey(Key.complex(1, 10)).endKey(Key.complex
                (2000, 5000)).build().getResponse().getKeys();

        assertThat(result.size(), is(3));
    }

    /**
     * Assert that passing an array of integers in queryPage
     * will produce a result list of two docs.
     */
    @Test
    public void queryPageWithStartAndEndIntKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "created").newPaginatedRequest
                (Key.Type.COMPLEX, Object.class).startKey(Key.complex(1, 10)).endKey(Key.complex
                (1001, 2000)).rowsPerPage(30).build().getResponse().getValues();

        assertThat(result.size(), is(2));
    }

    /**
     * Assert that passing a key in query with an explicit object array of a
     * string and integer value will produce a result list with three docs.
     */
    @Test
    public void queryWithStartAndEndObjectKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "creator_created").newRequest
                (Key.Type.COMPLEX, Object.class).startKey(Key.complex("uuid").add(1)).endKey(Key
                .complex("uuid").add(1010)).build().getResponse()
                .getValues();

        assertThat(result.size(), is(3));
    }

    /**
     * Assert that passing a key in queryPage with an explicit object array of
     * a string and integer value will produce a result list with three docs.
     */
    @Test
    public void queryPageWithStartAndEndObjectKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "creator_created")
                .newPaginatedRequest(Key.Type.COMPLEX, Object.class).startKey(Key.complex("uuid")
                        .add(1)).endKey(Key.complex("uuid").add(1010)
                ).rowsPerPage(30).build().getResponse().getKeys();

        assertThat(result.size(), is(3));
    }

    /**
     * Assert that passing a key in query with an array of number
     * values will produce a result list with two docs.
     */
    @Test
    public void queryWithStartAndEndNumbersKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "created_total")
                .newRequest
                        (Key.Type.COMPLEX, Object.class).startKey(Key.complex(1000, 12.00))
                .endKey(Key
                        .complex(1002, 15.00)).build().getResponse().getKeys();

        assertThat(result.size(), is(2));
    }

    /**
     * Assert that passing a key in queryPage with an array of number
     * values will produce a result list with one doc.
     */
    @Test
    public void queryPageWithStartAndEndNumbersKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "total_created")
                .newPaginatedRequest(Key.Type.COMPLEX, Object.class).startKey(Key.complex(10.00,
                        1)).endKey(Key.complex(11.00, 2000)).rowsPerPage(30).build().getResponse
                        ().getKeys();

        assertThat(result.size(), is(1));
    }

    /**
     * Assert that passing a key in query with an array of a boolean, string,
     * and integer value will produce a result list with three docs.
     */
    @Test
    public void queryWithStartAndEndBooleanStringIntegerKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "boolean_creator_created")
                .newRequest(Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex(false).add("uuid").add(1))
                .endKey(Key.complex(true).add("uuid").add(2000))
                .build().getResponse().getValues();

        assertThat(result.size(), is(3));
    }

    /**
     * Assert that passing a key in queryPage with an array of a boolean, string,
     * and integer value will produce a result list with one doc.
     */
    @Test
    public void queryPageWithStartAndEndBooleanStringIntegerKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "boolean_creator_created")
                .newPaginatedRequest(Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex(false).add("uuid").add(1))
                .endKey(Key.complex(false).add("uuid").add(2000))
                .rowsPerPage(30).build().getResponse().getValues();

        assertThat(result.size(), is(1));
    }

    /**
     * Assert that passing a key in query with an array of a integer, boolean,
     * and string value will produce a result list with one doc.
     */
    @Test
    public void queryWithStartAndEndIntegerBooleanStringKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "created_boolean_creator")
                .newRequest(Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex(1000).add(false).add("uuid"))
                .endKey(Key.complex(1000).add(false).add("uuid"))
                .build().getResponse().getValues();

        assertThat(result.size(), is(1));
    }

    /**
     * Assert that passing a key in queryPage with an array of a integer, boolean,
     * and string value will produce a result list with two docs.
     */
    @Test
    public void queryPageWithStartAndEndIntegerBooleanStringKeyArray() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "created_boolean_creator")
                .newPaginatedRequest
                        (Key.Type.COMPLEX, Object.class)
                .startKey(Key.complex(1000).add(false).add("uuid"))
                .endKey(Key.complex(1002).add(false).add("uuid"))
                .rowsPerPage(30).build().getResponse().getValues();

        assertThat(result.size(), is(2));
    }

    /**
     * Assert that passing a complex key with integer, string, and
     * boolean objects in query will produce a result with
     * JSON object values.
     * Assert that the keys and values in the query result's
     * JSON objects are the same as the expected objects.
     */
    @Test
    public void queryWithComplexStartEndKeyAndJsonObjectValue() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "boolean_creator_created")
                .newRequest
                        (Key.Type.COMPLEX, JsonObject.class)
                .startKey(Key.complex(true).add("uuid").add(1))
                .endKey(Key.complex(true).add("uuid").add(2000))
                .build().getResponse().getValues();

        assertThat(result.size(), is(2));

        Gson gson = new Gson();
        ArrayList expectedJsonObject = new ArrayList(),
                actualJsonObject = new ArrayList();

        for (int i = 0; i < result.size(); i++) {
            expectedJsonObject.add(multiValueKeyInit(null, i + 1));
            //Build a list from the query's results of the JSON objects from 'contentArray'
            JsonObject jsonValueObject = result.get(i);
            JsonObject actualJsonContentObject = jsonValueObject.get("contentArray")
                    .getAsJsonArray().get(0).getAsJsonObject();
            actualJsonObject.add(actualJsonContentObject);
        }

        assertJsonObjectKeysAndValues(expectedJsonObject, actualJsonObject);
    }

    /**
     * Assert that passing a complex key with integer, string, and
     * boolean objects in queryView will produce a result with
     * JSON object values.
     * Assert that the keys and values in the page result's
     * JSON objects are the same as the expected objects.
     */
    @Test
    public void queryPageWithComplexStartEndKeyAndJsonObjectValue() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "boolean_creator_created")
                .newPaginatedRequest
                        (Key.Type.COMPLEX, JsonObject.class).startKey(Key.complex(true)
                        .add("uuid").add(1))
                .endKey(Key.complex(true).add("uuid").add(2000)).rowsPerPage(30)
                .build().getResponse()
                .getValues();

        assertThat(result.size(), is(2));

        Gson gson = new Gson();
        ArrayList expectedJsonObject = new ArrayList(),
                actualJsonObject = new ArrayList();

        for (int i = 0; i < result.size(); i++) {
            expectedJsonObject.add(multiValueKeyInit(null, i + 1));
            //Build a list from the query's results of the JSON objects from 'contentArray'
            JsonObject actualJsonContentObject = (gson.toJsonTree(result.get(i)))
                    .getAsJsonObject().get("contentArray")
                    .getAsJsonArray().get(0).getAsJsonObject();
            actualJsonObject.add(actualJsonContentObject);
        }

        assertJsonObjectKeysAndValues(expectedJsonObject, actualJsonObject);
    }

    /**
     * Assert that passing a complex key with integer, string, and boolean objects, using the
     * high unicode value represented by addHighSentinel(), in queryView will produce a
     * result with JSON object values.
     * Assert that the keys and values in the page result's
     * JSON objects are the same as the expected objects.
     */
    @Test
    public void queryPageWithComplexStartEndKeyAndJsonObjectValueUsingEndKey() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "boolean_creator_created")
                .newPaginatedRequest
                        (Key.Type.COMPLEX, JsonObject.class).startKey(Key.complex(true)
                        .add("uuid").add(1))
                .endKey(Key.complex(true).add("uuid").addHighSentinel()).rowsPerPage(30)
                .build().getResponse()
                .getValues();

        assertThat(result.size(), is(2));

        Gson gson = new Gson();
        ArrayList expectedJsonObject = new ArrayList(),
                actualJsonObject = new ArrayList();

        for (int i = 0; i < result.size(); i++) {
            expectedJsonObject.add(multiValueKeyInit(null, i + 1));
            //Build a list from the query's results of the JSON objects from 'contentArray'
            JsonObject actualJsonContentObject = (gson.toJsonTree(result.get(i)))
                    .getAsJsonObject().get("contentArray")
                    .getAsJsonArray().get(0).getAsJsonObject();
            actualJsonObject.add(actualJsonContentObject);
        }

        assertJsonObjectKeysAndValues(expectedJsonObject, actualJsonObject);
    }

    /**
     * Assert that passing a complex key with integer, string, and
     * boolean objects in query will produce a result with
     * JSON array values.
     * Assert that the keys and values in the query result's
     * JSON arrays are the same as the expected arrays.
     */
    @Test
    public void queryWithComplexStartEndKeyAndJsonArrayValue() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "creator_boolean_total")
                .newRequest
                        (Key.Type.COMPLEX, JsonArray.class).startKey(Key.complex("uuid")
                        .add(true).add(1))
                .endKey(Key.complex("uuid").add(true).add(2000)).build()
                .getResponse()
                .getValues();

        assertThat(result.size(), is(2));

        Gson gson = new Gson();
        ArrayList expectedJsonArray = new ArrayList(),
                actualJsonArray = new ArrayList();


        for (int i = 0; i < result.size(); i++) {
            JsonArray expectedArray = new JsonArray();
            expectedArray.add(gson.toJsonTree("key-" + (i + 2)));
            expectedArray.add(gson.toJsonTree(10.009999999999999787 + i + 1));
            expectedJsonArray.add(expectedArray);
            //Build a list from the query's results of the JSON value array
            JsonArray actualJsonArrayValue = result.get(i);
            actualJsonArray.add(actualJsonArrayValue);
        }

        assertJsonArrayKeysAndValues(expectedJsonArray, actualJsonArray);
    }

    /**
     * Assert that passing a complex key with integer, string, and
     * boolean objects in queryView will produce a result with
     * JSON array values.
     * Assert that the keys and values in the page result's
     * JSON arrays are the same as the expected arrays.
     */
    @Test
    public void queryPageWithComplexStartEndKeyAndJsonArrayValue() throws Exception {
        init();

        List result = db.getViewRequestBuilder("example", "creator_boolean_total")
                .newPaginatedRequest
                        (Key.Type.COMPLEX, JsonArray.class).startKey(Key.complex("uuid")
                        .add(true).add(1))
                .endKey(Key.complex("uuid").add(true).add(2000)).rowsPerPage(30)
                .build().getResponse()
                .getValues();

        assertThat(result.size(), is(2));

        Gson gson = new Gson();
        ArrayList expectedJsonArray = new ArrayList(),
                actualJsonArray = new ArrayList();

        for (int i = 0; i < result.size(); i++) {
            JsonArray expectedArray = new JsonArray();
            expectedArray.add(gson.toJsonTree("key-" + (i + 2)));
            expectedArray.add(gson.toJsonTree(10.009999999999999787 + i + 1));
            expectedJsonArray.add(expectedArray);
            //Build a list from the query's results of the JSON value array
            JsonArray actualJsonArrayValue = result.get(i);
            actualJsonArray.add(actualJsonArrayValue);
        }

        assertJsonArrayKeysAndValues(expectedJsonArray, actualJsonArray);
    }

    @Test
    public void byComplexKey() throws Exception {
        init();

        List foos = db.getViewRequestBuilder("example", "by_date").newRequest(Key.Type.COMPLEX,
                Object.class).includeDocs(true).keys(Key.complex(2011, 10, 15)).reduce(false)
                .build().getResponse().getDocsAs(Foo
                        .class);

        assertThat(foos.size(), is(2));
    }

    @Test
    public void byComplexKeys() throws Exception {
        init();
        Key.ComplexKey complexKey1 = Key.complex(new Integer[]{2011, 10, 15});
        Key.ComplexKey complexKey2 = Key.complex(new Integer[]{2013, 12, 17});

        List foos = db.getViewRequestBuilder("example", "by_date").newRequest(Key.Type.COMPLEX,
                Object.class).includeDocs(true).keys(complexKey1, complexKey2).reduce(false)
                .build().getResponse().getDocsAs(Foo
                        .class);

        assertThat(foos.size(), is(3));
    }

    @Test
    public void viewResultEntries() throws Exception {
        init();

        Collection> rows = db.getViewRequestBuilder
                ("example",
                        "by_date").newRequest(Key.Type.COMPLEX,
                String.class).reduce(false).build().getResponse().getRows();

        assertThat(rows.size(), is(3));
    }

    @Test
    public void scalarValues() throws Exception {
        init();

        ViewRequestBuilder builder = db.getViewRequestBuilder("example", "by_tag");

        int allTags = builder.newRequest(Key.Type.STRING, int.class).build().getSingleValue();
        assertThat(allTags, is(4));

        long couchDbTags = builder.newRequest(Key.Type.STRING, long.class).keys("couchdb").build()
                .getSingleValue();
        assertThat(couchDbTags, is(2L));

        String javaTags = builder.newRequest(Key.Type.STRING, String.class).keys("java").build()
                .getSingleValue();
        assertThat(javaTags, is("1"));
    }

    @Test
    public void viewWithNoResult_emptyList() throws IOException {
        init();
        assertEquals(0, db.getViewRequestBuilder("example", "by_tag").newRequest(Key.Type.STRING,
                Object.class).keys("javax").build().getResponse().getKeys().size(), "The results " +
                "list should be of length 0");

    }

    @Test
    public void viewWithNoResult_nullSingleResult() throws IOException {
        init();
        assertNull(db.getViewRequestBuilder("example", "by_tag").newRequest(Key.Type.STRING,
                Object.class).keys("javax").build().getSingleValue(), "The single result should " +
                "be null");

    }

    @Test
    public void groupLevel() throws Exception {
        init();
        List result = db.getViewRequestBuilder("example", "by_date").newRequest(Key.Type
                .COMPLEX, Integer.class).groupLevel(2).build().getResponse().getValues();
        assertThat(result.size(), is(2));
    }

    @Test
    public void allDocs() throws Exception {
        init();
        db.save(new Foo());
        List allDocIds = db.getAllDocsRequestBuilder().build().getResponse().getDocIds();
        assertThat(allDocIds.size(), not(0));
        Map idsAndRevs = db.getAllDocsRequestBuilder().build().getResponse()
                .getIdsAndRevs();
        assertThat(idsAndRevs.size(), not(0));
        for (Map.Entry doc : idsAndRevs.entrySet()) {
            assertNotNull(doc.getValue(), "The document _rev value should not be null");
        }
    }

    @Test
    public void allDocsWithKeys() throws Exception {
        init();
        String id1 = db.save(new Foo()).getId();
        String id2 = db.save(new Foo()).getId();
        //create 3 and 4, but we don't care about the IDs
        db.save(new Foo()).getId();
        db.save(new Foo()).getId();

        List allDocIds = db.getAllDocsRequestBuilder().keys(id1, id2).build().getResponse()
                .getDocIds();
        assertThat(allDocIds.size(), is(2));
    }

    // all docs with given keys, one document deleted:
    // check that with include_docs false we can retrieve "sparse" document info including id, rev,
    // and deleted flag for the deleted and non-deleted documents
    @Test
    public void allDocsWithKeysDocDeleted() throws Exception {
        init();
        // delete one object, see that it is retrieved with deleted flag set
        Foo foo1 = new Foo();
        Foo foo2 = new Foo();
        Response response1 = db.save(foo1);
        Response response2 = db.save(foo2);
        Response removeResponse = db.remove(response2.getId(), response2.getRev());
        AllDocsResponse response = db.getAllDocsRequestBuilder().includeDocs(false).keys(response1.getId(), response2.getId()).build().getResponse();
        assertThat(response.getDocs(), hasSize(2));
        assertThat(response.getDocs().get(0).isDeleted(), is(false));
        assertThat(response.getDocs().get(0).getId(), is(response1.getId()));
        assertThat(response.getDocs().get(0).getRevision(), is(response1.getRev()));
        assertThat(response.getDocs().get(1).isDeleted(), is(true));
        assertThat(response.getDocs().get(1).getId(), is(response2.getId()));
        assertThat(response.getDocs().get(1).getRevision(), is(removeResponse.getRev()));
        assertThat(response.getTotalRowCount(), is(8L));
    }

    // all docs with given keys, one document deleted:
    // check that with include_docs true we can retrieve "sparse" document info including id, rev,
    // and deleted flag for the deleted and non-deleted documents
    @Test
    public void allDocsWithKeysDocDeletedIncludeDocs() throws Exception {
        init();
        // delete one object, see that it is retrieved with deleted flag set
        Foo foo1 = new Foo();
        Foo foo2 = new Foo();
        Response response1 = db.save(foo1);
        Response response2 = db.save(foo2);
        Response removeResponse = db.remove(response2.getId(), response2.getRev());
        AllDocsResponse response = db.getAllDocsRequestBuilder().includeDocs(true).keys(response1.getId(), response2.getId()).build().getResponse();
        assertThat(response.getDocs(), hasSize(2));
        assertThat(response.getDocs().get(0).isDeleted(), is(false));
        assertThat(response.getDocs().get(0).getId(), is(response1.getId()));
        assertThat(response.getDocs().get(0).getRevision(), is(response1.getRev()));
        assertThat(response.getDocs().get(1).isDeleted(), is(true));
        assertThat(response.getDocs().get(1).getId(), is(response2.getId()));
        assertThat(response.getDocs().get(1).getRevision(), is(removeResponse.getRev()));
    }

    // all docs with given keys, one document deleted:
    // check that with include_docs true we can deserialise when there is a deleted document
    @Test
    public void allDocsWithKeysDocDeletedIncludeDocsGetDocsAs() throws Exception {
        init();
        // delete one object, see that it is retrieved with deleted flag set
        Foo foo1 = new Foo();
        foo1.setContent("some content");
        Foo foo2 = new Foo();
        Response response1 = db.save(foo1);
        Response response2 = db.save(foo2);
        Response response3 = db.remove(response2.getId(), response2.getRev());

        List foos = db.getAllDocsRequestBuilder().includeDocs(true).keys(response1.getId(), response2.getId()).build().getResponse().getDocsAs(Foo.class);
        assertThat(foos, hasSize(2));
        assertThat(foos.get(0).getContent(), is("some content"));
        // Assert the deleted one has:
        // is deleted
        assertEquals(true, foos.get(1)._deleted);
        // the deleted rev
        assertEquals(response3.getRev(), foos.get(1).get_rev());
        // is empty or null content
        assertThat(foos.get(1).getContent(), isEmptyOrNullString());
    }


    @Test
    public void allDocsWithOneNonExistingKey() throws Exception {
        init();
        String id1 = db.save(new Foo()).getId();
        String id2 = "non-existing-doc";
        //create 3 and 4, but we don't care about the IDs
        db.save(new Foo()).getId();
        db.save(new Foo()).getId();

        AllDocsResponse response = db.getAllDocsRequestBuilder()
                .keys(id1, id2)
                .includeDocs(true)
                .build()
                .getResponse();

        Map errors = response.getErrors();
        Map idsAndRevs = response.getIdsAndRevs();
        assertThat(idsAndRevs.size(), is(1));
        for (Map.Entry doc : idsAndRevs.entrySet()) {
            assertNotNull(doc.getValue(), "The document _rev value should not be null");
        }

        assertThat(errors.size(), is(1));
        for (Map.Entry error : errors.entrySet()) {
            assertThat(error.getKey(), is("non-existing-doc"));
            assertThat(error.getValue(), is("not_found"));
        }
    }

    @Test
    public void allDocsWithOnlyNonExistingKeys() throws Exception {
        init();
        String id1 = "non-existing-doc";
        String id2 = "another-non-existing-doc";

        AllDocsResponse response = db.getAllDocsRequestBuilder()
                .keys(id1, id2)
                .includeDocs(true)
                .build()
                .getResponse();

        Map errors = response.getErrors();
        Map idsAndRevs = response.getIdsAndRevs();
        assertThat(idsAndRevs.size(), is(0));

        assertThat(errors.size(), is(2));
        for (Map.Entry error : errors.entrySet()) {
            assertThat(error.getValue(), is("not_found"));
        }
    }

    @Test
    public void allDocsEmptyListWithNonExistingKeys() throws Exception {
        init();
        String id1 = "non-existing-doc";
        String id2 = "another-non-existing-doc";

        List response = db.getAllDocsRequestBuilder()
                .keys(id1, id2)
                .includeDocs(true)
                .build()
                .getResponse()
                .getDocs();

        assertNotNull(response);
        assertThat(response.size(), is(0));
    }

    @Test
    public void allDocsWithLargeNumberOfKeys() throws Exception {
        init();
        int nDocs = 200;
        List ids = new ArrayList();
        for (int i=0; i allDocIds = db.getAllDocsRequestBuilder().keys(ids.toArray(new String[]{}))
                .build()
                .getResponse()
                .getDocIds();
        assertThat(allDocIds.size(), is(nDocs/2));
    }

    @Test
    public void allDocsWithUnicodeKeys() throws Exception {
        init();
        db.save(new Foo("日本"));
        List allDocIds = db.getAllDocsRequestBuilder().keys("日本")
                .build()
                .getResponse()
                .getDocIds();
        assertThat(allDocIds.size(), is(1));
    }

    /**
     * Assert that IllegalStateException is thrown if any keys are added after calling
     * addHighSentinel()
     */
    @Test
    public void addingAfterHighSentinelThrowsError() {
        assertThrows(IllegalStateException.class, () ->  {
            Key.complex("ABC").addHighSentinel().add("XYZ");
        });
    }

    /**
     * @param index the index to encode.
     * @return a three character string representing the given {@code index}.
     */
    public static String docTitle(int index) {
        return String.format("%03d", index);
    }

    private void init() {
        Foo foo = null;

        foo = new Foo("id-1", "key-1");
        foo.setTags(Arrays.asList(new String[]{"couchdb", "views"}));
        foo.setComplexDate(new int[]{2011, 10, 15});
        multiValueKeyInit(foo, 0);
        db.save(foo);

        foo = new Foo("id-2", "key-2");
        foo.setTags(Arrays.asList(new String[]{"java", "couchdb"}));
        foo.setComplexDate(new int[]{2011, 10, 15});
        multiValueKeyInit(foo, 1);
        db.save(foo);

        foo = new Foo("id-3", "key-3");
        foo.setComplexDate(new int[]{2013, 12, 17});
        multiValueKeyInit(foo, 2);
        db.save(foo);
    }

    public static JsonObject multiValueKeyInit(Foo foo, int i) {
        //JSON object for multi value key array tests
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("creator", "uuid");
        jsonObject.addProperty("created", 1000 + i);
        jsonObject.addProperty("boolean", (i != 0));
        jsonObject.addProperty("total", 10.01 + i);
        jsonObject.addProperty("quotes", "\"quotes\" " + String.valueOf(i));
        jsonObject.addProperty("spaces", " spaces " + String.valueOf(i));
        jsonObject.addProperty("letters", (char) ('a' + i) + "bc");
        jsonObject.addProperty("one", 1);

        JsonArray jsonArray = new JsonArray();
        jsonArray.add(jsonObject);

        if (foo != null) {
            foo.setContentArray(jsonArray);
        }

        return jsonObject;
    }

    /**
     * Helper method to assert that keys and values in two JSON objects are equal.
     *
     * @param expectedJson the expected JSON element(s)
     * @param actualJson   the actual JSON element(s) from test result list
     */
    private void assertJsonObjectKeysAndValues(ArrayList expectedJson,
                                               ArrayList actualJson) {

        assertEquals(expectedJson.size(),
                actualJson.size());

        for (int i = 0; i < actualJson.size(); i++) {
            JsonObject expectedJsonObject = expectedJson.get(i).getAsJsonObject();
            JsonObject actualJsonObject = actualJson.get(i).getAsJsonObject();

            Iterator> actualJsonIter =
                    actualJsonObject.entrySet().iterator();

            for (Map.Entry expectedJsonMap :
                    expectedJsonObject.entrySet()) {
                String expectedJsonKey = expectedJsonMap.getKey();
                JsonElement expectedJsonValue = expectedJsonMap.getValue();

                if (actualJsonIter.hasNext()) {
                    Map.Entry actualJsonMap = actualJsonIter.next();
                    assertEquals(expectedJsonKey, actualJsonMap.getKey());
                    assertEquals(expectedJsonValue, actualJsonMap.getValue());
                }
            }
        }
    }

    /**
     * Helper method to assert that keys and values in two JSON arrays are equal.
     *
     * @param expectedJson the expected JSON element(s)
     * @param actualJson   the actual JSON element(s) from test result list
     */
    private void assertJsonArrayKeysAndValues(ArrayList expectedJson,
                                              ArrayList actualJson) {
        assertEquals(expectedJson.size(), actualJson.size());

        for (int i = 0; i < actualJson.size(); i++) {
            //Check key and values in the JSON array
            JsonArray expectedJsonArray = expectedJson.get(i).getAsJsonArray();
            JsonArray actualJsonArray = actualJson.get(i).getAsJsonArray();

            assertEquals(expectedJsonArray.size(),
                    actualJsonArray.size());

            Iterator actualJsonIter =
                    actualJsonArray.iterator();

            Iterator expectedJsonIter =
                    expectedJsonArray.iterator();

            while (expectedJsonIter.hasNext()) {
                JsonElement expectedJsonElement = expectedJsonIter.next();
                JsonElement actualJsonElement = actualJsonIter.next();

                assertEquals(expectedJsonElement, actualJsonElement);
            }
        }
    }

    /**
     * Validate that it is possible to POST multiple requests to a view query and that the
     * results of each request are as expected.
     *
     * @throws IOException
     */
    @Test
    @RequiresCloudant
    public void multiRequest() throws IOException {
        init();
        ViewMultipleRequest multi = db.getViewRequestBuilder("example", "foo")
                .newMultipleRequest(Key.Type.STRING, Object.class)
                .keys("key-1").add()
                .keys("key-2").add()
                .keys("key-3").add()
                .keys("key-1","key-3").add()
                .build();
        int i = 1;
        List> responses = multi.getViewResponses();
        assertEquals(4, responses.size(), "There should be 4 responses for 4 requests");
        for (ViewResponse response : responses) {
            if (i <= 3) {
                assertEquals(1, response.getRows().size(), "There should be 1 row in each response");
                assertEquals("key-" + i, response.getKeys().get(0), "The returned key should be key-"
                        + i);
            } else {
                assertEquals(2, response.getRows().size(), "There should be 2 rows in the response");
                assertEquals(Arrays.asList("key-1", "key-3"), response.getKeys(), "The returned keys should match");
            }
            i++;
        }
    }

    /**
     * Validate that a request without parameters can be built after calling add().
     */
    @Test
    public void multiRequestBuildSingle() {
        ViewMultipleRequest multi = db.getViewRequestBuilder("example", "foo")
                .newMultipleRequest(Key.Type.STRING, Object.class)
                .add()
                .build();
    }

    /**
     * Validate that a multi request with parameters can be built after calling add().
     */
    @Test
    public void multiRequestBuildParametersSingle() {
        ViewMultipleRequest multi = db.getViewRequestBuilder("example", "foo")
                .newMultipleRequest(Key.Type.STRING, Object.class)
                .keys("key-1").add()
                .build();
    }

    /**
     * Validate that a multi request with parameters can be built after calling add().
     */
    @Test
    public void multiRequestBuildParametersMulti() {
        ViewMultipleRequest multi = db.getViewRequestBuilder("example", "foo")
                .newMultipleRequest(Key.Type.STRING, Object.class)
                .keys("key-1").add()
                .keys("key-2").add()
                .build();
    }

    /**
     * Validate that a multi request with parameters can be built after calling add().
     */
    @Test
    public void multiRequestBuildParametersFirst() {
        ViewMultipleRequest multi = db.getViewRequestBuilder("example", "foo")
                .newMultipleRequest(Key.Type.STRING, Object.class)
                .keys("key-1").add()
                .add()
                .build();
    }

    /**
     * Validate that a multi request with parameters can be built after calling add().
     */
    @Test
    public void multiRequestBuildParametersSecond() {
        ViewMultipleRequest multi = db.getViewRequestBuilder("example", "foo")
                .newMultipleRequest(Key.Type.STRING, Object.class)
                .add()
                .keys("key-2").add()
                .build();
    }

    /**
     * Validate that an IllegalStateException is thrown if an attempt is made to build a multi
     * request without calling add() before build() with two requests.
     */
    @Test
    public void multiRequestBuildOnlyAfterAdd() {
        assertThrows(IllegalStateException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewMultipleRequest multi = db.getViewRequestBuilder
                                ("example", "foo")
                                .newMultipleRequest(Key.Type.STRING, Object.class)
                                .keys("key-1").add()
                                .keys("key-2").build();
                    }
                });
    }

    /**
     * Validate that an IllegalStateException is thrown if an attempt is made to build a multi
     * request without calling add() before build() with a single request with parameters.
     */
    @Test
    public void multiRequestBuildOnlyAfterAddSingle() {
        assertThrows(IllegalStateException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewMultipleRequest multi = db.getViewRequestBuilder
                                ("example", "foo")
                                .newMultipleRequest(Key.Type.STRING, Object.class)
                                .keys("key-1")
                                .build();
                    }
                });
    }

    /**
     * Validate that an IllegalStateException is thrown if an attempt is made to build a multi
     * request without calling add() before build() with a single request with no view request
     * parameter calls.
     */
    @Test
    public void multiRequestBuildOnlyAfterAddNoParams() {
        assertThrows(IllegalStateException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewMultipleRequest multi = db.getViewRequestBuilder
                                ("example", "foo")
                                .newMultipleRequest(Key.Type.STRING, Object.class)
                                .build();
                    }
                });
    }

    /**
     * Validate that it is possible to POST multiple requests to a view query mixing reduced and not
     * reduced queries and that the results of each request are as expected.
     *
     * @throws IOException
     */
    @Test
    @RequiresCloudant
    public void multiRequestMixedReduced() throws IOException {
        init();
        ViewMultipleRequest multi = db.getViewRequestBuilder("example", "by_tag")
                .newMultipleRequest(Key.Type.STRING, Object.class)
                /* add includeDocs args after https://issues.apache.org/jira/browse/COUCHDB-3070 */
                .reduce(false).keys("java")/*.includeDocs(true)*/.add()
                .reduce(true)/*.includeDocs(false)*/.add()
                .build();

        List> responses = multi.getViewResponses();
        assertEquals(2, responses.size(), "There should be 2 responses for 2 requests");

        List javaTagKeys = responses.get(0).getKeys();
        assertEquals(1, javaTagKeys.size(), "There should be 1 java tag result");
        assertEquals("java", javaTagKeys.get(0), "The key should be java");

        List allTagsReduced = responses.get(1).getValues();
        assertEquals(1, allTagsReduced.size(), "There should be 1 reduced result");
        assertEquals(4, ((Number) allTagsReduced.get(0)).intValue(), "The result should be 4");
    }

    /**
     * Assert that no additional pages are available on an unpaginated request even if additional
     * results are available.
     *
     * @throws Exception
     */
    @Test
    public void assertNoPagesOnUnpaginated() throws Exception {
        init();
        ViewResponse response = db.getViewRequestBuilder("example", "foo")
                .newRequest(Key.Type.STRING,
                        Object.class).limit(2).build().getResponse();
        assertEquals(2, response.getKeys().size(), "There should be 2 keys returned");
        assertFalse(response.hasNextPage(), "There should be no additional pages");
        assertNull(response.nextPage(), "The next page should be null");
    }

    /**
     * Validate that it is possible to loop through pages using an enhanced for loop. Verify that
     * the results are expected on each page.
     *
     * @throws Exception
     */
    @Test
    public void enhancedForPagination() throws Exception {
        init();
        ViewRequest paginatedQuery = db.getViewRequestBuilder("example", "foo")
                .newPaginatedRequest(Key.Type.STRING,
                        Object.class).rowsPerPage(1).build();
        int i = 1;
        for (ViewResponse page : paginatedQuery.getResponse()) {
            assertEquals(1, page.getKeys().size(), "There should be one key on each page");
            assertEquals("key-" + i, page.getKeys().get(0), "The key should be key-" + i);
            i++;
        }
    }

    /**
     * Assert that an IllegalArgumentException is thrown when rowsPerPage exceeds MAX_INT-1.
     *
     * @throws Exception
     */
    @Test
    public void rowsPerPageValidationMax() throws Exception {
        assertThrows(IllegalArgumentException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewRequest paginatedQuery = db.getViewRequestBuilder
                                ("example", "foo")
                                .newPaginatedRequest(Key.Type.STRING,
                                        Object.class).rowsPerPage(Integer.MAX_VALUE).build();
                    }
                });
    }

    /**
     * Assert that an IllegalArgumentException is thrown when rowsPerPage is zero.
     *
     * @throws Exception
     */
    @Test
    public void rowsPerPageValidationZero() throws Exception {
        assertThrows(IllegalArgumentException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewRequest paginatedQuery = db.getViewRequestBuilder
                                ("example", "foo")
                                .newPaginatedRequest(Key.Type.STRING,
                                        Object.class).rowsPerPage(0).build();
                    }
                });
    }

    /**
     * Assert that an IllegalArgumentException is thrown when rowsPerPage is negative.
     *
     * @throws Exception
     */
    @Test
    public void rowsPerPageValidationMin() throws Exception {
        assertThrows(IllegalArgumentException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewRequest paginatedQuery = db.getViewRequestBuilder
                                ("example", "foo")
                                .newPaginatedRequest(Key.Type.STRING,
                                        Object.class).rowsPerPage(-25).build();
                    }
                });
    }

    /**
     * Assert that an IllegalArgumentException is thrown when specifying both reduce=true and
     * include_docs=true
     *
     * @throws Exception
     */
    @Test
    public void validationIncludeDocsReduceView() throws Exception {
        assertThrows(IllegalArgumentException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewRequest paginatedQuery = db.getViewRequestBuilder
                                ("example", "foo")
                                .newRequest(Key.Type.STRING,
                                        Object.class).includeDocs(true).reduce(true).build();
                    }
                });
    }

    /**
     * Test that no validation exception is thrown when reduce is not specified, but include_docs
     * is true. This is because whilst reduce=true and include_docs=true are mutually exclusive
     * and even though reduce defaults to true, if the view does not actually have a reduce function
     * then include_docs is still valid on the server.
     *
     * @throws Exception
     */
    @Test
    public void noExceptionWhenReduceTrueByDefault() throws Exception {
        ViewRequest paginatedQuery = db.getViewRequestBuilder("example", "foo")
                .newRequest(Key.Type.STRING,
                        Object.class).includeDocs(true).build();
    }

    /**
     * Tests than an IllegalArgumentException is thrown when group_level is set without using a
     * complex key.
     *
     * @throws Exception
     */
    @Test
    public void validationGroupLevelWithSimpleKey() throws Exception {
        assertThrows(IllegalArgumentException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewRequest paginatedQuery = db.getViewRequestBuilder
                                ("example", "foo")
                                .newRequest(Key.Type.STRING,
                                        Object.class).groupLevel(1).build();
                    }
                });
    }

    /**
     * Tests than an IllegalArgumentException is thrown when group_level is set without using a
     * reduce view.
     *
     * @throws Exception
     */
    @Test
    public void validationGroupLevelWithNonReduce() throws Exception {
        assertThrows(IllegalArgumentException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewRequest paginatedQuery = db.getViewRequestBuilder
                                ("example", "foo")
                                .newRequest(Key.Type.STRING,
                                        Object.class).reduce(false).groupLevel(1).build();
                    }
                });
    }

    /**
     * Tests than an IllegalArgumentException is thrown when group is set without using a
     * reduce view.
     *
     * @throws Exception
     */
    @Test
    public void validationGroupWithNonReduce() throws Exception {
        assertThrows(IllegalArgumentException.class,
                new Executable() {
                    @Override
                    public void execute() throws Throwable {
                        ViewRequest paginatedQuery = db.getViewRequestBuilder
                                ("example", "foo")
                                .newRequest(Key.Type.STRING,
                                        Object.class).reduce(false).group(true).build();
                    }
                });
    }

    /**
     * Test written for https://github.com/cloudant/java-cloudant/issues/172 where complex key
     * integers were being changed to floats when using pagination tokens. For example ["uuid", 1]
     * would become ["uuid", 1.0] when trying to request the second page. 1.0 sorts before 1, so the
     * wrong documents would be returned on the second page.
     *
     * The test creates 10 documents that emit complex keys of e.g. ["uuid", 1000]
     *
     * We use 5 documents per page and use a start key of ["uuid", 1].
     *
     * The test gets the first page and then retrieves the second page using a token.
     * It captures the request URL and then asserts that the startkey was of the correct integer
     * form.
     *
     * @throws Exception
     */
    @Test
    public void testComplexKeyContainingIntTokenPagination() throws Exception {

        // Use the intercepted client as we want to check the query string in the connection
        db = interceptedDB.get();
        Utils.putDesignDocs(db);

        // Create 10 documents in the database
        int nDocs = 10;
        for (int i = 0; i < nDocs; i++) {
            Foo f = new Foo("" + i);
            f.setPosition(i);
            multiValueKeyInit(f, i);
            db.save(f);
        }

        // Use the creator_created view (complex keys [String, int])
        ViewRequest request = db.getViewRequestBuilder("example",
                "creator_created").newPaginatedRequest(Key.Type.COMPLEX, Object.class).startKey(Key
                .complex("uuid").add(1)).rowsPerPage(5).build();

        // Get the second page response by token
        ViewResponse response = request.getResponse();
        String token = response.getNextPageToken();
        request.getResponse(token);

        // We want the last context
        HttpConnectionInterceptorContext context = cci.contexts.get(cci.contexts.size() - 1);
        String query = context.connection.url.getQuery();
        assertTrue(query.contains("startkey=%5B%22uuid%22," + "1005%5D"), "The query startkey " +
                "should match.");
    }

    /**
     * Tests that reserved characters in a view parameter are encoded.
     * https://github.com/cloudant/java-cloudant/issues/202
     *
     * @throws Exception
     */
    @Test
    public void testUriReservedCharsInStartKey() throws Exception {
        char[] reservedChars = new char[]{
                // 3986 general delimeters
                ':', '/', '?', '#', '[', ']', '@',
                // 3986 sub delimeters
                '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='};
        for (char c : reservedChars) {
            testUriReservedCharInStartKey(c);
        }
    }

    private void testUriReservedCharInStartKey(char c) throws Exception {
        try {
            ViewRequest request = db.getViewRequestBuilder("example",
                    "foo").newPaginatedRequest(Key.Type.STRING, Object.class)
                    .startKey("a" + c + "b")
                    .rowsPerPage(5)
                    .build();
            request.getResponse();
            // We don't actually need to do anything with the response, just ensure the request does
            // not cause an exception.
        } catch (Exception e) {
            fail("The character " + c + " caused an exception to be thrown.");
        }
    }


    /**
     * 

* Test added for https://github.com/cloudant/java-cloudant/issues/297 *

*

* When _all_docs is used an array of rows is returned containing an entry for each key * specified. If the document doesn't exist an "error" : "not_found" entry is present in the row * instead of the expected "value" property. Trying to use the value results in a NPE. *

* * @throws Exception */ @Test public void getIdsAndRevsForTwoNonExistentKeysWithAllDocs() throws Exception { db.getAllDocsRequestBuilder().keys(new String[]{"a", "b"}).build().getResponse() .getIdsAndRevs(); } }