/*
* Copyright (c) 2016 Couchbase, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.couchbase.client.java.view;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.couchbase.client.core.message.ResponseStatus;
import com.couchbase.client.core.message.view.ViewQueryResponse;
import com.couchbase.client.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.deps.io.netty.buffer.Unpooled;
import com.couchbase.client.deps.io.netty.util.CharsetUtil;
import com.couchbase.client.java.AsyncBucket;
import com.couchbase.client.java.Bucket;
import com.couchbase.client.java.CouchbaseBucket;
import com.couchbase.client.java.SerializationHelper;
import com.couchbase.client.java.document.JsonDocument;
import com.couchbase.client.java.document.RawJsonDocument;
import com.couchbase.client.java.document.json.JsonArray;
import com.couchbase.client.java.document.json.JsonObject;
import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func1;
/**
* Verifies the correct functionality of the {@link ViewQuery} DSL.
*
* @author Michael Nitschinger
* @since 2.0
*/
public class ViewQueryTest {
@Test
public void shouldSetDefaults() {
ViewQuery query = ViewQuery.from("design", "view");
assertEquals("design", query.getDesign());
assertEquals("view", query.getView());
assertFalse(query.isDevelopment());
assertEquals("", query.toQueryString());
assertEquals("ViewQuery(design/view){params=\"\"}", query.toString());
}
@Test
public void shouldReduce() {
ViewQuery query = ViewQuery.from("design", "view").reduce();
assertEquals("reduce=true", query.toQueryString());
query = ViewQuery.from("design", "view").reduce(true);
assertEquals("reduce=true", query.toQueryString());
query = ViewQuery.from("design", "view").reduce(false);
assertEquals("reduce=false", query.toQueryString());
}
@Test
public void shouldLimit() {
ViewQuery query = ViewQuery.from("design", "view").limit(10);
assertEquals("limit=10", query.toQueryString());
}
@Test
public void shouldSkip() {
ViewQuery query = ViewQuery.from("design", "view").skip(3);
assertEquals("skip=3", query.toQueryString());
}
@Test
public void shouldGroup() {
ViewQuery query = ViewQuery.from("design", "view").group();
assertEquals("group=true", query.toQueryString());
query = ViewQuery.from("design", "view").group(false);
assertEquals("group=false", query.toQueryString());
}
@Test
public void shouldGroupLevel() {
ViewQuery query = ViewQuery.from("design", "view").groupLevel(2);
assertEquals("group_level=2", query.toQueryString());
}
@Test
public void shouldSetInclusiveEnd() {
ViewQuery query = ViewQuery.from("design", "view").inclusiveEnd();
assertEquals("inclusive_end=true", query.toQueryString());
query = ViewQuery.from("design", "view").inclusiveEnd(false);
assertEquals("inclusive_end=false", query.toQueryString());
}
@Test
public void shouldSetStale() {
ViewQuery query = ViewQuery.from("design", "view").stale(Stale.FALSE);
assertEquals("stale=false", query.toQueryString());
query = ViewQuery.from("design", "view").stale(Stale.TRUE);
assertEquals("stale=ok", query.toQueryString());
query = ViewQuery.from("design", "view").stale(Stale.UPDATE_AFTER);
assertEquals("stale=update_after", query.toQueryString());
}
@Test
public void shouldSetOnError() {
ViewQuery query = ViewQuery.from("design", "view").onError(OnError.CONTINUE);
assertEquals("on_error=continue", query.toQueryString());
query = ViewQuery.from("design", "view").onError(OnError.STOP);
assertEquals("on_error=stop", query.toQueryString());
}
@Test
public void shouldSetDebug() {
ViewQuery query = ViewQuery.from("design", "view").debug();
assertEquals("debug=true", query.toQueryString());
query = ViewQuery.from("design", "view").debug(false);
assertEquals("debug=false", query.toQueryString());
}
@Test
public void shouldSetDescending() {
ViewQuery query = ViewQuery.from("design", "view").descending();
assertEquals("descending=true", query.toQueryString());
query = ViewQuery.from("design", "view").descending(false);
assertEquals("descending=false", query.toQueryString());
}
@Test
public void shouldHandleKey() {
ViewQuery query = ViewQuery.from("design", "view").key("key");
assertEquals("key=%22key%22", query.toQueryString());
query = ViewQuery.from("design", "view").key(1);
assertEquals("key=1", query.toQueryString());
query = ViewQuery.from("design", "view").key(true);
assertEquals("key=true", query.toQueryString());
query = ViewQuery.from("design", "view").key(3.55);
assertEquals("key=3.55", query.toQueryString());
query = ViewQuery.from("design", "view").key(JsonArray.from("foo", 3));
assertEquals("key=%5B%22foo%22%2C3%5D", query.toQueryString());
query = ViewQuery.from("design", "view").key(JsonObject.empty().put("foo", true));
assertEquals("key=%7B%22foo%22%3Atrue%7D", query.toQueryString());
}
@Test
public void shouldHandleKeys() {
JsonArray keysArray = JsonArray.from("foo", 3, true);
ViewQuery query = ViewQuery.from("design", "view").keys(keysArray);
assertEquals("", query.toQueryString());
assertEquals(keysArray.toString(), query.getKeys());
}
@Test
public void shouldOutputSmallKeysInToString() {
JsonArray keysArray = JsonArray.from("foo", 3, true);
ViewQuery query = ViewQuery.from("design", "view").keys(keysArray);
assertEquals("", query.toQueryString());
assertEquals("ViewQuery(design/view){params=\"\", keys=\"[\"foo\",3,true]\"}", query.toString());
}
@Test
public void shouldTruncateLargeKeysInToString() {
StringBuilder largeString = new StringBuilder(142);
for (int i = 0; i < 140; i++) {
largeString.append('a');
}
largeString.append("bc");
JsonArray keysArray = JsonArray.from(largeString.toString());
ViewQuery query = ViewQuery.from("design", "view").keys(keysArray);
assertEquals("", query.toQueryString());
assertEquals("ViewQuery(design/view){params=\"\", keys=\"[\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +
"aaaaaaaaaaaaaaaaaaaaaaaaaaa...\"(146 chars total)}", query.toString());
}
@Test
public void shouldOutputDesignDocViewDevAndIncludeDocsInToString() {
ViewQuery query = ViewQuery.from("a", "b").includeDocs().development();
assertEquals("", query.toQueryString());
assertEquals("ViewQuery(a/b){params=\"\", dev, includeDocs}", query.toString());
}
@Test
public void shouldHandleStartKey() {
ViewQuery query = ViewQuery.from("design", "view").startKey("key");
assertEquals("startkey=%22key%22", query.toQueryString());
query = ViewQuery.from("design", "view").startKey(1);
assertEquals("startkey=1", query.toQueryString());
query = ViewQuery.from("design", "view").startKey(true);
assertEquals("startkey=true", query.toQueryString());
query = ViewQuery.from("design", "view").startKey(3.55);
assertEquals("startkey=3.55", query.toQueryString());
query = ViewQuery.from("design", "view").startKey(JsonArray.from("foo", 3));
assertEquals("startkey=%5B%22foo%22%2C3%5D", query.toQueryString());
query = ViewQuery.from("design", "view").startKey(JsonObject.empty().put("foo", true));
assertEquals("startkey=%7B%22foo%22%3Atrue%7D", query.toQueryString());
}
@Test
public void shouldHandleEndKey() {
ViewQuery query = ViewQuery.from("design", "view").endKey("key");
assertEquals("endkey=%22key%22", query.toQueryString());
query = ViewQuery.from("design", "view").endKey(1);
assertEquals("endkey=1", query.toQueryString());
query = ViewQuery.from("design", "view").endKey(true);
assertEquals("endkey=true", query.toQueryString());
query = ViewQuery.from("design", "view").endKey(3.55);
assertEquals("endkey=3.55", query.toQueryString());
query = ViewQuery.from("design", "view").endKey(JsonArray.from("foo", 3));
assertEquals("endkey=%5B%22foo%22%2C3%5D", query.toQueryString());
query = ViewQuery.from("design", "view").endKey(JsonObject.empty().put("foo", true));
assertEquals("endkey=%7B%22foo%22%3Atrue%7D", query.toQueryString());
}
@Test
public void shouldHandleStartKeyDocID() {
ViewQuery query = ViewQuery.from("design", "view").startKeyDocId("mykey");
assertEquals("startkey_docid=mykey", query.toQueryString());
}
@Test
public void shouldHandleEndKeyDocID() {
ViewQuery query = ViewQuery.from("design", "view").endKeyDocId("mykey");
assertEquals("endkey_docid=mykey", query.toQueryString());
}
@Test
public void shouldRespectDevelopmentParam() {
ViewQuery query = ViewQuery.from("design", "view").development(true);
assertTrue(query.isDevelopment());
query = ViewQuery.from("design", "view").development(false);
assertFalse(query.isDevelopment());
}
@Test
public void shouldConcatMoreParams() {
ViewQuery query = ViewQuery.from("design", "view")
.descending()
.debug()
.development()
.group()
.reduce(false)
.startKey(JsonArray.from("foo", true));
assertEquals("reduce=false&group=true&debug=true&descending=true&startkey=%5B%22foo%22%2Ctrue%5D",
query.toQueryString());
}
@Test(expected = IllegalArgumentException.class)
public void shouldDisallowNegativeLimit() {
ViewQuery.from("design", "view").limit(-1);
}
@Test(expected = IllegalArgumentException.class)
public void shouldDisallowNegativeSkip() {
ViewQuery.from("design", "view").skip(-1);
}
@Test
public void shouldToggleDevelopment() {
ViewQuery query = ViewQuery.from("design", "view").development(true);
assertTrue(query.isDevelopment());
query = ViewQuery.from("design", "view").development(false);
assertFalse(query.isDevelopment());
}
@Test
public void shouldSupportSerialization() throws Exception {
ViewQuery query = ViewQuery.from("design", "view")
.descending()
.debug()
.development()
.group()
.reduce(false)
.keys(JsonArray.from("1", "2"))
.startKey(JsonArray.from("foo", true));
byte[] serialized = SerializationHelper.serializeToBytes(query);
assertNotNull(serialized);
ViewQuery deserialized = SerializationHelper.deserializeFromBytes(serialized, ViewQuery.class);
assertEquals(query, deserialized);
}
@Test
public void shouldIncludeDocs() {
ViewQuery query = ViewQuery.from("design", "view").includeDocs();
assertTrue(query.isIncludeDocs());
assertEquals(JsonDocument.class, query.includeDocsTarget());
query = ViewQuery.from("design", "view").includeDocs(JsonDocument.class);
assertTrue(query.isIncludeDocs());
assertEquals(JsonDocument.class, query.includeDocsTarget());
query = ViewQuery.from("design", "view");
assertFalse(query.isIncludeDocs());
assertNull(query.includeDocsTarget());
query = ViewQuery.from("design", "view").includeDocs(false, RawJsonDocument.class);
assertFalse(query.isIncludeDocs());
assertEquals(RawJsonDocument.class, query.includeDocsTarget());
}
@Test
public void shouldStoreKeysAsJsonOutsideParams() {
JsonArray keys = JsonArray.create().add("1").add("2").add("3");
String keysJson = keys.toString();
ViewQuery query = ViewQuery.from("design", "view");
assertNull(query.getKeys());
query.keys(keys);
assertEquals(keysJson, query.getKeys());
assertFalse(query.toQueryString().contains("keys="));
assertFalse(query.toQueryString().contains("3"));
}
@Test
public void shouldFlagOrderRetainedWhenUsingIncludeDocsOrdered() {
ViewQuery query1 = ViewQuery.from("a", "b").includeDocsOrdered();
ViewQuery query2 = ViewQuery.from("a", "b").includeDocsOrdered(true);
ViewQuery query3 = ViewQuery.from("a", "b").includeDocsOrdered(JsonDocument.class);
ViewQuery query4 = ViewQuery.from("a", "b").includeDocsOrdered(true, JsonDocument.class);
assertEquals(true, query1.isOrderRetained());
assertEquals(true, query2.isOrderRetained());
assertEquals(true, query3.isOrderRetained());
assertEquals(true, query4.isOrderRetained());
}
@Test
public void shouldDeactivateOrderRetainedWhenSettingIncludeDocsOrderedToFalse() {
ViewQuery query1 = ViewQuery.from("a", "b").includeDocsOrdered();
assertEquals(true, query1.isOrderRetained());
query1.includeDocsOrdered(false);
assertEquals(false, query1.isOrderRetained());
ViewQuery query2 = ViewQuery.from("a", "b").includeDocsOrdered(true, JsonDocument.class);
assertEquals(true, query2.isOrderRetained());
query2.includeDocsOrdered(false, JsonDocument.class);
assertEquals(false, query2.isOrderRetained());
}
@Test
public void shouldLoadDocumentsOutOfOrderWithIncludeDocs() {
StringBuilder trace = new StringBuilder();
Bucket bucket = mockDelayedBucket(2, trace, "A", "B", "C", "D");
ViewResult result = bucket.query(ViewQuery.from("any", "view")
.includeDocs());
//to assert reception is out of order
String[] expected = new String[]{"C", "D", "A", "B"};
//to assert requests are in order, emissions are out of order (A and B delayed)
String expectedTrace = "\nGET A\nGET B\nGET C\nGot C\nGET D\nGot D\nDelayed A by 100ms\nGot A\nDelayed B by 200ms\nGot B";
assertOrder(expected, expectedTrace, result.allRows(), trace.toString());
}
@Test
public void shouldLoadDocumentsInOrderWithIncludeDocsOrdered() {
StringBuilder trace = new StringBuilder();
Bucket bucket = mockDelayedBucket(2, trace, "A", "B", "C", "D");
ViewResult result = bucket.query(ViewQuery.from("any", "view")
.includeDocsOrdered());
//to assert reception is in order
String[] expectedIds = new String[]{"A", "B", "C", "D"};
//to assert requests are in order and emissions are out of order (A and B delayed)
String expectedTrace = "\nGET A\nGET B\nGET C\nGot C\nGET D\nGot D\nDelayed A by 100ms\nGot A\nDelayed B by 200ms\nGot B";
assertOrder(expectedIds, expectedTrace, result.allRows(), trace.toString());
}
private void assertOrder(String[] expectedIds, String expectedTrace, List<ViewRow> rows, String trace) {
for (int i = 0; i < rows.size(); i++) {
ViewRow row = rows.get(i);
assertNotNull(row);
JsonDocument doc = row.document();
assertEquals(row.id(), doc.id());
assertEquals(expectedIds[i], row.id());
}
assertEquals(expectedTrace, trace);
}
private Bucket mockDelayedBucket(final int numberDelayed, final StringBuilder trace, final String... keys) {
final Set<String> delayed = new HashSet<String>(numberDelayed);
delayed.addAll(Arrays.asList(keys).subList(0, numberDelayed));
List<ByteBuf> fakeRows = new ArrayList<ByteBuf>(keys.length);
for (String key : keys) {
String fakeRowJson = JsonObject.create()
.put("id", key)
.toString();
ByteBuf fakeBuffer = Unpooled.copiedBuffer(fakeRowJson, CharsetUtil.UTF_8);
fakeRows.add(fakeBuffer);
}
final Observable fakeRowObs = Observable.from(fakeRows);
final AtomicInteger delay = new AtomicInteger(100);
final AsyncBucket spyBucket = Mockito.mock(AsyncBucket.class);
//this will induce a delay on the first n keys when includeDocs' get is triggered, and trace the invocations
when(spyBucket.get(Matchers.anyString(), any(Class.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
String key = (String) invocation.getArguments()[0];
Observable<JsonDocument> obs = Observable.just(JsonDocument.create(key))
.doOnNext(new Action1<JsonDocument>() {
@Override
public void call(JsonDocument jsonDocument) {
trace.append("\nGET ").append(jsonDocument.id());
}
});
if (delayed.contains(key)) {
final int d = delay.getAndAdd(100);
obs = obs.delay(d, TimeUnit.MILLISECONDS)
.doOnNext(new Action1<JsonDocument>() {
@Override
public void call(JsonDocument jsonDocument) {
trace.append("\nDelayed ").append(jsonDocument.id()).append(" by ").append(d).append("ms");
}
});
}
return obs.doOnNext(new Action1<JsonDocument>() {
@Override
public void call(JsonDocument jsonDocument) {
trace.append("\nGot ").append(jsonDocument.id());
}
});
}
});
//this simulates a view response with the preconstructed buffers above, and calls the view result
//mapper so that it uses the mock get for its includeDocs calls.
when(spyBucket.query(any(ViewQuery.class))).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
final ViewQuery query = (ViewQuery) invocation.getArguments()[0];
ViewQueryResponse response = new ViewQueryResponse(fakeRowObs, Observable.<ByteBuf>empty(),
Observable.<String>empty(), 0, "", ResponseStatus.SUCCESS, null);
return Observable.just(response)
.flatMap(new Func1<ViewQueryResponse, Observable<AsyncViewResult>>() {
@Override
public Observable<AsyncViewResult> call(final ViewQueryResponse response) {
return ViewQueryResponseMapper.mapToViewResult(spyBucket, query, response);
}
});
}
});
return new CouchbaseBucket(spyBucket, DefaultCouchbaseEnvironment.create(), null, "", "", "");
}
}