/* * 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.core.cluster; import com.couchbase.client.core.message.ResponseStatus; import com.couchbase.client.core.message.kv.GetRequest; import com.couchbase.client.core.message.kv.GetResponse; import com.couchbase.client.core.message.kv.MutationToken; import com.couchbase.client.core.message.kv.RemoveRequest; import com.couchbase.client.core.message.kv.RemoveResponse; import com.couchbase.client.core.message.kv.UpsertRequest; import com.couchbase.client.core.message.kv.UpsertResponse; import com.couchbase.client.core.message.kv.subdoc.multi.Lookup; import com.couchbase.client.core.message.kv.subdoc.multi.LookupCommandBuilder; import com.couchbase.client.core.message.kv.subdoc.multi.MultiLookupResponse; import com.couchbase.client.core.message.kv.subdoc.multi.MultiMutationResponse; import com.couchbase.client.core.message.kv.subdoc.multi.MultiResult; import com.couchbase.client.core.message.kv.subdoc.multi.Mutation; import com.couchbase.client.core.message.kv.subdoc.multi.MutationCommand; import com.couchbase.client.core.message.kv.subdoc.multi.MutationCommandBuilder; import com.couchbase.client.core.message.kv.subdoc.multi.SubMultiLookupRequest; import com.couchbase.client.core.message.kv.subdoc.multi.SubMultiMutationRequest; import com.couchbase.client.core.message.kv.subdoc.simple.AbstractSubdocRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SimpleSubdocResponse; import com.couchbase.client.core.message.kv.subdoc.simple.SubArrayRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SubCounterRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SubDeleteRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SubDictAddRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SubDictUpsertRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SubExistRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SubGetRequest; import com.couchbase.client.core.message.kv.subdoc.simple.SubReplaceRequest; import com.couchbase.client.core.util.ClusterDependentTest; import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.ResourceLeakDetector; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import java.io.IOException; import java.util.Map; 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.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Verifies basic functionality of binary subdocument operations. * * @author Simon Baslé * @since 1.2 */ public class SubdocumentMessageTest extends ClusterDependentTest { static { ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID); } /** Use this key when you want previous data to exist in your test*/ private static final String testSubKey = "testSubKey"; /** Use this key when you want to create data in your test, to be cleaned up after*/ private static final String testInsertionSubKey = "testInsertionSubKey"; /** Use this key when you want existing data that is simply a JSON array at the root*/ private static final String testArrayRoot = "testArrayRootSubKey"; /** Use this key when you want existing data that include a deep complex array*/ private static final String testComplexSubArray = "testComplexSubArray"; private static final ObjectMapper JSON = new ObjectMapper(); private static final String jsonContent = "{\"value\":\"stringValue\", \"sub\": {\"value\": \"subStringValue\",\"array\": [\"array1\", 2, true]}}"; private static final String jsonArrayContent = "[\"v1\"]"; private static final String jsonComplexSubArrayContent = "{\"value\":\"stringValue\", \"complexArray\": [{\"v1\": 123}]}"; @BeforeClass public static void checkSubdocAvailable() throws Exception { connect(false); assumeMinimumVersionCompatible(4, 5); } @Before public void prepareData() { UpsertRequest upsert = new UpsertRequest(testSubKey, Unpooled.copiedBuffer(jsonContent, CharsetUtil.UTF_8), bucket(), true); UpsertResponse response = cluster().<UpsertResponse>send(upsert).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue("Couldn't insert " + testSubKey, response.status().isSuccess()); upsert = new UpsertRequest(testArrayRoot, Unpooled.copiedBuffer(jsonArrayContent, CharsetUtil.UTF_8), bucket(), true); response = cluster().<UpsertResponse>send(upsert).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue("Couldn't insert " + testArrayRoot, response.status().isSuccess()); upsert = new UpsertRequest(testComplexSubArray, Unpooled.copiedBuffer(jsonComplexSubArrayContent, CharsetUtil.UTF_8), bucket(), true); response = cluster().<UpsertResponse>send(upsert).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue("Couldn't insert " + testComplexSubArray, response.status().isSuccess()); } @After public void deleteData() { RemoveRequest remove = new RemoveRequest(testSubKey, bucket()); RemoveResponse response = cluster().<RemoveResponse>send(remove).toBlocking().single(); boolean removedSubkey = response.status().isSuccess(); ReferenceCountUtil.releaseLater(response.content()); remove = new RemoveRequest(testInsertionSubKey, bucket()); response = cluster().<RemoveResponse>send(remove).toBlocking().single(); boolean removedInsertionSubkey = response.status().isSuccess() || response.status().equals(ResponseStatus.NOT_EXISTS); ReferenceCountUtil.releaseLater(response.content()); remove = new RemoveRequest(testArrayRoot, bucket()); response = cluster().<RemoveResponse>send(remove).toBlocking().single(); boolean removedArraySubkey = response.status().isSuccess(); ReferenceCountUtil.releaseLater(response.content()); remove = new RemoveRequest(testComplexSubArray, bucket()); response = cluster().<RemoveResponse>send(remove).toBlocking().single(); boolean removeComplexSubArray = response.status().isSuccess(); ReferenceCountUtil.releaseLater(response.content()); assertTrue("Couldn't remove " + testSubKey, removedSubkey); assertTrue("Couldn't remove " + testInsertionSubKey, removedInsertionSubkey); assertTrue("Couldn't remove " + testArrayRoot, removedArraySubkey); assertTrue("Couldn't remove " + testComplexSubArray, removeComplexSubArray); } @Test public void shouldThrowNullPointerIfPathIsNull() { boolean npeOnGet = false; boolean npeOnMutation = false; try { new SubGetRequest(testSubKey, null, bucket()); } catch (NullPointerException e) { npeOnGet = true; } try { new SubReplaceRequest(testSubKey, null, Unpooled.copiedBuffer("test", CharsetUtil.UTF_8), bucket()); } catch (NullPointerException e) { npeOnMutation = true; } assertTrue("Expected null path get to fail with NPE", npeOnGet); assertTrue("Expected null path mutation to fail with NPE", npeOnMutation); } @Test public void shouldSubGetValueInExistingDocumentObject() throws Exception{ String subValuePath = "sub.value"; SubGetRequest valueRequest = new SubGetRequest(testSubKey, subValuePath, bucket()); SimpleSubdocResponse valueResponse = cluster().<SimpleSubdocResponse>send(valueRequest).toBlocking().single(); String raw = valueResponse.content().toString(CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(valueResponse.content()); assertNotNull(raw); assertEquals("\"subStringValue\"", raw); } @Test public void shouldSubGetValueInExistingDocumentArray() throws Exception{ String subArrayPath = "sub.array[1]"; SubGetRequest arrayRequest = new SubGetRequest(testSubKey, subArrayPath, bucket()); SimpleSubdocResponse arrayResponse = cluster().<SimpleSubdocResponse>send(arrayRequest).toBlocking().single(); String raw = arrayResponse.content().toString(CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(arrayResponse.content()); assertNotNull(raw); assertEquals("2", raw); } @Test public void shouldFailSubGetIfPathDoesntExist() { String wrongPath = "sub.arra[1]"; SubGetRequest badRequest = new SubGetRequest(testSubKey, wrongPath, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(badRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertFalse(response.status().isSuccess()); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status()); } @Test public void shouldSucceedExistOnDictionnaryPath() { String path = "sub.value"; SubExistRequest request = new SubExistRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); } @Test public void shouldSucceedExistOnArrayPath() { String path = "sub.array[1]"; SubExistRequest request = new SubExistRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); } @Test public void shouldReturnPathNotFoundOnExistWithBadPath() { String path = "sub.bad"; SubExistRequest request = new SubExistRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status());; } @Test public void testExistDoesntContainValueOnSuccess() { String path = "sub.array[1]"; SubExistRequest request = new SubExistRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertEquals(0, response.content().readableBytes()); } @Test public void shouldDictUpsertInExistingDocumentObject() { String subPath = "sub.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); //verify initial state assertMutation(testSubKey, jsonContent); //mutate SubDictUpsertRequest upsertRequest = new SubDictUpsertRequest(testSubKey, subPath, fragment, bucket()); SimpleSubdocResponse upsertResponse = cluster().<SimpleSubdocResponse>send(upsertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(upsertResponse.content()); assertTrue(upsertResponse.status().isSuccess()); //verify mutated state assertMutation(testSubKey, jsonContent.replace("subStringValue", "mutated")); } @Test public void shouldReturnPathInvalidOnDictUpsertInArray() { String subPath = "sub.array[1]"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); //mutate SubDictUpsertRequest upsertRequest = new SubDictUpsertRequest(testSubKey, subPath, fragment, bucket()); SimpleSubdocResponse upsertResponse = cluster().<SimpleSubdocResponse>send(upsertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(upsertResponse.content()); assertFalse(upsertResponse.status().isSuccess()); assertEquals(0, upsertResponse.content().readableBytes()); assertEquals(ResponseStatus.SUBDOC_PATH_INVALID, upsertResponse.status()); } @Test public void shouldGetMutationTokenWithMutation() throws Exception { String subPath = "sub.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubDictUpsertRequest upsertRequest = new SubDictUpsertRequest(testSubKey, subPath, fragment, bucket()); SimpleSubdocResponse upsertResponse = cluster().<SimpleSubdocResponse>send(upsertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(upsertResponse.content()); assertValidMetadata(upsertResponse.mutationToken()); } @Test public void shouldNotGetMutationTokenWithGet() throws Exception { String subPath = "sub.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubGetRequest getRequest = new SubGetRequest(testSubKey, subPath, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(getRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertNull(response.mutationToken()); } @Test public void shouldDictAddOnSubObject() { String subPath = "sub.otherValue"; ByteBuf fragment = Unpooled.copiedBuffer("\"inserted\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); //mutate SubDictAddRequest insertRequest = new SubDictAddRequest(testSubKey, subPath, fragment, bucket()); SimpleSubdocResponse insertResponse = cluster().<SimpleSubdocResponse>send(insertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(insertResponse.content()); assertTrue(insertResponse.status().isSuccess()); assertEquals(0, insertResponse.content().readableBytes()); //check the insertion at the end of "sub" object String expected = "{\"value\":\"stringValue\", \"sub\": {\"value\": \"subStringValue\",\"array\": [\"array1\", 2, true]" + ",\"otherValue\":\"inserted\"}}"; assertMutation(testSubKey, expected); } @Test public void shouldReturnPathExistOnDictAddOnSubValue() { String subPath = "sub.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); //mutate SubDictAddRequest insertRequest = new SubDictAddRequest(testSubKey, subPath, fragment, bucket()); SimpleSubdocResponse insertResponse = cluster().<SimpleSubdocResponse>send(insertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(insertResponse.content()); assertFalse(insertResponse.status().isSuccess()); assertEquals(0, insertResponse.content().readableBytes()); assertEquals(ResponseStatus.SUBDOC_PATH_EXISTS, insertResponse.status()); } @Test public void shouldReturnPathNotFoundOnDictAddForNewDeepPath() { String subPath = "sub2.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"insertedPath\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); //mutate SubDictAddRequest insertRequest = new SubDictAddRequest(testSubKey, subPath, fragment, bucket()); assertFalse(insertRequest.createIntermediaryPath()); SimpleSubdocResponse insertResponse = cluster().<SimpleSubdocResponse>send(insertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(insertResponse.content()); assertFalse(insertResponse.status().isSuccess()); assertEquals(0, insertResponse.content().readableBytes()); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, insertResponse.status()); } @Test public void shouldReturnPathInvalidOnDictAddForArrayPath() { String subPath = "sub.array[1]"; ByteBuf fragment = Unpooled.copiedBuffer("\"insertedPath\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); //mutate SubDictAddRequest insertRequest = new SubDictAddRequest(testSubKey, subPath, fragment, bucket()); assertFalse(insertRequest.createIntermediaryPath()); SimpleSubdocResponse insertResponse = cluster().<SimpleSubdocResponse>send(insertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(insertResponse.content()); assertFalse(insertResponse.status().isSuccess()); assertEquals(0, insertResponse.content().readableBytes()); assertEquals(ResponseStatus.SUBDOC_PATH_INVALID, insertResponse.status()); } @Test public void shouldCreateIntermediaryNodesOnDictAddForNewDeepPathIfForced() { String subPath = "sub2.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"insertedPath\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); //mutate SubDictAddRequest insertRequest = new SubDictAddRequest(testSubKey, subPath, fragment, bucket()); insertRequest.createIntermediaryPath(true); SimpleSubdocResponse insertResponse = cluster().<SimpleSubdocResponse>send(insertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(insertResponse.content()); assertTrue(insertResponse.status().isSuccess()); assertEquals(0, insertResponse.content().readableBytes()); } @Test public void shouldNotCreateIntermediaryNodesOnDictAddForNewDeepPathByDefault() { String subPath = "sub2.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"insertedPath\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubDictAddRequest insertRequest = new SubDictAddRequest(testSubKey, subPath, fragment, bucket()); SimpleSubdocResponse insertResponse = cluster().<SimpleSubdocResponse>send(insertRequest).toBlocking().single(); ReferenceCountUtil.releaseLater(insertResponse.content()); assertFalse(insertResponse.status().isSuccess()); assertEquals(0, insertResponse.content().readableBytes()); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, insertResponse.status()); } @Test public void shouldDeleteExistingObjectPath() { String path = "sub.value"; SubDeleteRequest request = new SubDeleteRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue(response.status().isSuccess()); assertEquals(0, response.content().readableBytes()); assertEquals(ResponseStatus.SUCCESS, response.status()); //assert the mutation String expected = "{\"value\":\"stringValue\", \"sub\": {\"array\": [\"array1\", 2, true]}}"; assertMutation(testSubKey, expected); } @Test public void shouldDeleteExistingArrayIndexPath() { String path = "sub.array[1]"; SubDeleteRequest request = new SubDeleteRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue(response.status().isSuccess()); assertEquals(0, response.content().readableBytes()); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, "{\"value\":\"stringValue\", \"sub\": {\"value\": \"subStringValue\",\"array\": [\"array1\", true]}}"); } @Test public void shouldDeleteExistingWholeArrayPath() { String path = "sub.array"; SubDeleteRequest request = new SubDeleteRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue(response.status().isSuccess()); assertEquals(0, response.content().readableBytes()); assertEquals(ResponseStatus.SUCCESS, response.status()); //assert the mutation String expected = "{\"value\":\"stringValue\", \"sub\": {\"value\": \"subStringValue\"}}"; assertMutation(testSubKey, expected); } @Test public void shouldReturnPathNotFoundOnDeleteNonexistingPath() { String path = "sub.bad"; SubDeleteRequest request = new SubDeleteRequest(testSubKey, path, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertFalse(response.status().isSuccess()); assertEquals(0, response.content().readableBytes()); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status()); } @Test public void shouldThrowIllegalArgumentExceptionOnOperationsNotPermittingEmptyPath() { String path = ""; ByteBuf fragment = Unpooled.EMPTY_BUFFER; String operation; operation = "GET"; try { new SubGetRequest(testSubKey, path, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } operation = "EXIST"; try { new SubExistRequest(testSubKey, path, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } operation = "DELETE"; try { new SubDeleteRequest(testSubKey, path, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } operation = "ARRAY INSERT"; try { new SubArrayRequest(testSubKey, path, SubArrayRequest.ArrayOperation.INSERT, fragment, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } operation = "COUNTER"; try { new SubCounterRequest(testSubKey, path, 21L, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } operation = "DICT_ADD"; try { new SubDictAddRequest(testSubKey, path, fragment, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } operation = "DICT_UPSERT"; try { new SubDictUpsertRequest(testSubKey, path, fragment, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } operation = "REPLACE"; try { new SubReplaceRequest(testSubKey, path, fragment, bucket()); fail("Expected IllegalArgumentException for " + operation); } catch (IllegalArgumentException e) { assertSame("Expected emtpy path IllegalArgumentException for " + operation, AbstractSubdocRequest.EXCEPTION_EMPTY_PATH, e); } } @Test public void shouldReplaceValueInSubObject() { String path = "sub.value"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubReplaceRequest request = new SubReplaceRequest(testSubKey, path, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue(response.status().isSuccess()); assertEquals(0, response.content().readableBytes()); assertEquals(ResponseStatus.SUCCESS, response.status()); //assert the mutation String expected = "{\"value\":\"stringValue\", \"sub\": {\"value\": \"mutated\",\"array\": [\"array1\", 2, true]}}"; assertMutation(testSubKey, expected); } @Test public void shouldReplaceEntryInArray() { String path = "sub.array[0]"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubReplaceRequest request = new SubReplaceRequest(testSubKey, path, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertTrue(response.status().isSuccess()); assertEquals(0, response.content().readableBytes()); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, "{\"value\":\"stringValue\", \"sub\": {\"value\": \"subStringValue\",\"array\": [\"mutated\", 2, true]}}"); } @Test public void shouldReturnPathNotFoundOnReplaceNonExistingPath() { String path = "sub.value2"; ByteBuf fragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubReplaceRequest request = new SubReplaceRequest(testSubKey, path, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); assertFalse(response.status().isSuccess()); assertEquals(0, response.content().readableBytes()); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status()); } @Test public void shouldAllowArrayOperationOnDocumentRootIfArray() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.PUSH_FIRST; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testArrayRoot, "", arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); } @Test public void shouldNotAllowArrayOperationOnDocumentRootIfNotArray() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.PUSH_LAST; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, "", arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_MISMATCH, response.status()); } @Test public void shouldReturnPathMismatchForArrayOperationOnNonArrayFinalElement() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.PUSH_FIRST; String path = "sub"; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_MISMATCH, response.status()); } @Test public void shouldPushLastInArray() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.PUSH_LAST; String path = "sub.array"; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, jsonContent.replace("]", ", \"arrayElement\"]")); } @Test public void shouldReturnPathNotFoundOnPushLastOnNonExistingPath() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.PUSH_LAST; String path = "sub.array2"; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status()); } @Test public void shouldPushFirstInArray() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.PUSH_FIRST; String path = "sub.array"; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, jsonContent.replace("[", "[\"arrayElement\", ")); } @Test public void shouldReturnPathNotFoundOnPushFirstOnNonExistingPath() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.PUSH_FIRST; String path = "sub.array2"; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status()); } @Test public void shouldShiftArrayByOneOnArrayInsert() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.INSERT; String path = "sub.array[1]"; ByteBuf fragment = Unpooled.copiedBuffer("99", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, jsonContent.replace("2", "99, 2")); } @Test public void shouldPrependIfArrayInsertIndexIsZero() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.INSERT; String path = "sub.array[0]"; ByteBuf fragment = Unpooled.copiedBuffer("99", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, jsonContent.replace("[", "[99, ")); } @Test public void shouldAppendIfArrayInsertIndexIsSize() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.INSERT; String path = "sub.array[3]"; ByteBuf fragment = Unpooled.copiedBuffer("99", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, jsonContent.replace("]", ", 99]")); } @Test public void shouldReturnPathNotFoundOnArrayInsertOverSize() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.INSERT; String path = "sub.array[5]"; ByteBuf fragment = Unpooled.copiedBuffer("99", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status()); } @Test public void shouldReturnPathInvalidOnArrayInsertAtNegativeIndex() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.INSERT; String path = "sub.array[-1]"; ByteBuf fragment = Unpooled.copiedBuffer("99", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_INVALID, response.status()); } @Test public void shouldAddUniqueIfValueNotAlreadyInArray() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.ADD_UNIQUE; String path = "sub.array"; ByteBuf fragment = Unpooled.copiedBuffer("99", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertMutation(testSubKey, jsonContent.replace("]", ", 99]")); } @Test public void shouldReturnPathExistOnArrayAddUniqueWithDuplicateValue() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.ADD_UNIQUE; String path = "sub.array"; ByteBuf fragment = Unpooled.copiedBuffer("2", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testSubKey, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_EXISTS, response.status()); } @Test public void shouldReturnPathMismatchOnArrayAddUniqueIfArrayIsNotPrimitive() { SubArrayRequest.ArrayOperation arrayOp = SubArrayRequest.ArrayOperation.ADD_UNIQUE; String path = "complexArray"; ByteBuf fragment = Unpooled.copiedBuffer("\"arrayElement\"", CharsetUtil.UTF_8); ReferenceCountUtil.releaseLater(fragment); SubArrayRequest request = new SubArrayRequest(testComplexSubArray, path, arrayOp, fragment, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_PATH_MISMATCH, response.status()); } @Test public void shouldIncrementOnCounterWithPositiveDelta() { String path = "sub.array[1]"; long delta = 100L; long expected = 102L; SubCounterRequest request = new SubCounterRequest(testSubKey, path, delta, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); String result = response.content().toString(CharsetUtil.UTF_8); assertEquals(ResponseStatus.SUCCESS, response.status()); assertEquals(""+expected, result); assertEquals(expected, Long.parseLong(result)); } @Test public void shouldDecrementOnCounterWithNegativeDelta() { String path = "sub.array[1]"; long delta = -100L; long expected = -98L; SubCounterRequest request = new SubCounterRequest(testSubKey, path, delta, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); String result = response.content().toString(CharsetUtil.UTF_8); assertEquals(ResponseStatus.SUCCESS, response.status()); assertEquals(""+expected, result); assertEquals(expected, Long.parseLong(result)); } @Test public void shouldStartFromZeroAndApplyDeltaOnCounterInNewFinalPath() { String path = "counter"; long delta = 1234L; long expected = delta; SubCounterRequest request = new SubCounterRequest(testSubKey, path, delta, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); String result = response.content().toString(CharsetUtil.UTF_8); assertEquals(ResponseStatus.SUCCESS, response.status()); assertEquals(""+expected, result); assertEquals(expected, Long.parseLong(result)); } @Test public void shouldReturnPathNotExistOnCounterInIncompletePath() { String path = "counter.subCounter"; long delta = 99L; SubCounterRequest request = new SubCounterRequest(testSubKey, path, delta, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); String result = response.content().toString(CharsetUtil.UTF_8); assertEquals(0, result.length()); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.status()); } @Test public void shouldReturnDeltaRangeOnCounterDeltaZero() { String path = "counter"; long delta = 0L; SubCounterRequest request = new SubCounterRequest(testSubKey, path, delta, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); String result = response.content().toString(CharsetUtil.UTF_8); assertEquals(0, result.length()); assertEquals(ResponseStatus.SUBDOC_DELTA_RANGE, response.status()); } @Test public void shouldReturnDeltaRangeOnCounterDeltaOverflow() { String path = "counter"; long prepareOverFlow = 1L; long delta = Long.MAX_VALUE; //first request will bring the value to +1 SubCounterRequest request = new SubCounterRequest(testSubKey, path, prepareOverFlow, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); String result = response.content().toString(CharsetUtil.UTF_8); assertEquals("1", result); //second request will overflow request = new SubCounterRequest(testSubKey, path, delta, bucket()); response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); result = response.content().toString(CharsetUtil.UTF_8); assertEquals(result, 0, result.length()); /* * Was SUBDOC_DELTA_RANGE, but changed to VALUE_CANTINSERT between 4.5 dp and BETA. * See https://issues.couchbase.com/browse/JCBC-931, https://issues.couchbase.com/browse/MB-18169 */ assertEquals(ResponseStatus.SUBDOC_VALUE_CANTINSERT, response.status()); } @Test public void shouldReturnDeltaRangeOnCounterDeltaUnderflow() { String path = "counter"; long prepareUnderflow = -1L; long delta = Long.MIN_VALUE; //first request will bring the value to -1 SubCounterRequest request = new SubCounterRequest(testSubKey, path, prepareUnderflow, bucket()); SimpleSubdocResponse response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); String result = response.content().toString(CharsetUtil.UTF_8); assertEquals("-1", result); //second request will underflow request = new SubCounterRequest(testSubKey, path, delta, bucket()); response = cluster().<SimpleSubdocResponse>send(request).toBlocking().single(); ReferenceCountUtil.releaseLater(response.content()); result = response.content().toString(CharsetUtil.UTF_8); assertEquals(result, 0, result.length()); assertEquals(ResponseStatus.SUBDOC_DELTA_RANGE, response.status()); } @Test public void shouldHaveIndividualResultsOnSparseMultiLookup() { String expected = "EXIST(sub): SUCCESS\n" + "EXIST(sub2): SUBDOC_PATH_NOT_FOUND\n" + "GET(sub): SUCCESS = {\"value\": \"subStringValue\",\"array\": [\"array1\", 2, true]}\n" + "GET(sub.array[1]): SUCCESS = 2\n" + "GET(sub2): SUBDOC_PATH_NOT_FOUND\n"; SubMultiLookupRequest request = new SubMultiLookupRequest(testSubKey, bucket(), new LookupCommandBuilder(Lookup.EXIST, "sub").build(), new LookupCommandBuilder(Lookup.EXIST, "sub2").build(), new LookupCommandBuilder(Lookup.GET, "sub").build(), new LookupCommandBuilder(Lookup.GET, "sub.array[1]").build(), new LookupCommandBuilder(Lookup.GET, "sub2").build()); MultiLookupResponse response = cluster().<MultiLookupResponse>send(request).toBlocking().single(); assertEquals(Unpooled.EMPTY_BUFFER, response.content()); StringBuilder body = new StringBuilder(); for (MultiResult r : response.responses()) { body.append(r.toString()).append('\n'); ReferenceCountUtil.release(r.value()); } assertTrue(response.cas() != 0); assertEquals(ResponseStatus.SUBDOC_MULTI_PATH_FAILURE, response.status()); assertEquals(expected, body.toString()); } @Test public void shouldHaveIndividualResultsOnFullySuccessfulMultiLookup() { String expected = "EXIST(sub): SUCCESS\n" + "GET(sub.array[1]): SUCCESS = 2\n"; SubMultiLookupRequest request = new SubMultiLookupRequest(testSubKey, bucket(), new LookupCommandBuilder(Lookup.EXIST, "sub").build(), new LookupCommandBuilder(Lookup.GET, "sub.array[1]").build()); MultiLookupResponse response = cluster().<MultiLookupResponse>send(request).toBlocking().single(); assertEquals(Unpooled.EMPTY_BUFFER, response.content()); StringBuilder body = new StringBuilder(); for (MultiResult r : response.responses()) { body.append(r.toString()).append('\n'); ReferenceCountUtil.release(r.value()); } assertTrue(response.cas() != 0); assertEquals(ResponseStatus.SUCCESS, response.status()); assertEquals(expected, body.toString()); } @Test public void shouldApplyAllMultiMutationsAndReleaseCommandFragments() { ByteBuf counterFragment = Unpooled.copiedBuffer("-404", CharsetUtil.UTF_8); counterFragment.retain(2); ByteBuf stringFragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); stringFragment.retain(2); ByteBuf arrayInsertedFragment = Unpooled.copiedBuffer("\"inserted\"", CharsetUtil.UTF_8); ByteBuf arrayFirstFragment = Unpooled.copiedBuffer("\"first\"", CharsetUtil.UTF_8); ByteBuf arrayLastFragment = Unpooled.copiedBuffer("\"last\"", CharsetUtil.UTF_8); ByteBuf uniqueFragment = Unpooled.copiedBuffer("\"unique\"", CharsetUtil.UTF_8); MutationCommand[] commands = new MutationCommand[] { new MutationCommandBuilder(Mutation.COUNTER, "counter").fragment(counterFragment).build(), new MutationCommandBuilder(Mutation.COUNTER, "another.counter").fragment(counterFragment).createIntermediaryPath(true).build(), new MutationCommandBuilder(Mutation.COUNTER, "another.counter").fragment(counterFragment).build(), new MutationCommandBuilder(Mutation.DICT_ADD, "sub.value2").fragment(stringFragment).build(), new MutationCommandBuilder(Mutation.DICT_UPSERT, "sub.value3").fragment(stringFragment).build(), new MutationCommandBuilder(Mutation.REPLACE, "value").fragment(stringFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_INSERT, "sub.array[1]").fragment(arrayInsertedFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_PUSH_FIRST, "sub.array").fragment(arrayFirstFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_PUSH_LAST, "sub.array").fragment(arrayLastFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_ADD_UNIQUE, "sub.array").fragment(uniqueFragment).build(), new MutationCommandBuilder(Mutation.DELETE, "sub.value").build() }; SubMultiMutationRequest request = new SubMultiMutationRequest(testSubKey, bucket(), commands); MultiMutationResponse response = cluster().<MultiMutationResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUCCESS, response.status()); assertEquals(Unpooled.EMPTY_BUFFER, response.content()); assertEquals(-1, response.firstErrorIndex()); assertEquals(ResponseStatus.SUCCESS, response.firstErrorStatus()); assertEquals(0, stringFragment.refCnt()); assertEquals(0, counterFragment.refCnt()); assertEquals(0, arrayInsertedFragment.refCnt()); assertEquals(0, arrayFirstFragment.refCnt()); assertEquals(0, arrayLastFragment.refCnt()); assertEquals(0, uniqueFragment.refCnt()); StringBuilder sb = new StringBuilder(); for (int i = 0; i < commands.length; i++) { MutationCommand command = commands[i]; MultiResult result = response.responses().get(i); assertEquals(command.path(), result.path()); assertEquals(command.mutation(), result.operation()); sb.append('\n').append(result); ReferenceCountUtil.releaseLater(result.value()); } if (sb.length() > 1) sb.deleteCharAt(0); String expectedResponse = "COUNTER(counter): SUCCESS = -404" + "\nCOUNTER(another.counter): SUCCESS = -404" + "\nCOUNTER(another.counter): SUCCESS = -808" + //values below have no content "\nDICT_ADD(sub.value2): SUCCESS" + "\nDICT_UPSERT(sub.value3): SUCCESS" + "\nREPLACE(value): SUCCESS" + "\nARRAY_INSERT(sub.array[1]): SUCCESS" + "\nARRAY_PUSH_FIRST(sub.array): SUCCESS" + "\nARRAY_PUSH_LAST(sub.array): SUCCESS" + "\nARRAY_ADD_UNIQUE(sub.array): SUCCESS" + "\nDELETE(sub.value): SUCCESS"; assertEquals(expectedResponse, sb.toString()); String expected = "{\"value\":\"mutated\"," + "\"sub\":{" + // "\"value\":\"subStringValue\"," + //DELETED "\"array\":[\"first\",\"array1\",\"inserted\",2,true,\"last\",\"unique\"]" + ",\"value2\":\"mutated\"" + ",\"value3\":\"mutated\"}," + "\"counter\":-404," + "\"another\":{\"counter\":-808}}"; assertMutation(testSubKey, expected); } @Test public void shouldFailSomeMultiMutationsAndReleaseCommandFragments() { ByteBuf counterFragment = Unpooled.copiedBuffer("-404", CharsetUtil.UTF_8); counterFragment.retain(); ByteBuf stringFragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); stringFragment.retain(2); ByteBuf arrayInsertedFragment = Unpooled.copiedBuffer("\"inserted\"", CharsetUtil.UTF_8); ByteBuf arrayFirstFragment = Unpooled.copiedBuffer("\"first\"", CharsetUtil.UTF_8); ByteBuf arrayLastFragment = Unpooled.copiedBuffer("\"last\"", CharsetUtil.UTF_8); ByteBuf uniqueFragment = Unpooled.copiedBuffer("\"unique\"", CharsetUtil.UTF_8); SubMultiMutationRequest request = new SubMultiMutationRequest(testSubKey, bucket(), new MutationCommandBuilder(Mutation.COUNTER, "counter", counterFragment).build(), new MutationCommandBuilder(Mutation.COUNTER, "another.counter", counterFragment).createIntermediaryPath(true).build(), new MutationCommandBuilder(Mutation.DICT_ADD, "sub.value2", stringFragment).build(), new MutationCommandBuilder(Mutation.DICT_UPSERT, "sub.value3", stringFragment).build(), new MutationCommandBuilder(Mutation.REPLACE, "value", stringFragment).build(), //this one fails new MutationCommandBuilder(Mutation.ARRAY_INSERT, "sub.array[5]", arrayInsertedFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_PUSH_FIRST, "sub.array", arrayFirstFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_PUSH_LAST, "sub.array", arrayLastFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_ADD_UNIQUE, "sub.array", uniqueFragment).build(), //this one would also fail, but server stops at first failure new MutationCommandBuilder(Mutation.DELETE, "path.not.found").build() ); MultiMutationResponse response = cluster().<MultiMutationResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.SUBDOC_MULTI_PATH_FAILURE, response.status()); assertEquals(Unpooled.EMPTY_BUFFER, response.content()); assertEquals(5, response.firstErrorIndex()); assertEquals(ResponseStatus.SUBDOC_PATH_NOT_FOUND, response.firstErrorStatus()); assertEquals(0, response.responses().size()); assertEquals(0, stringFragment.refCnt()); assertEquals(0, counterFragment.refCnt()); assertEquals(0, arrayInsertedFragment.refCnt()); assertEquals(0, arrayFirstFragment.refCnt()); assertEquals(0, arrayLastFragment.refCnt()); assertEquals(0, uniqueFragment.refCnt()); //no mutation happened String expected = jsonContent; assertMutation(testSubKey, expected); } @Test public void shouldFailAllMultiMutationsAndReleaseCommandFragments() { ByteBuf counterFragment = Unpooled.copiedBuffer("-404", CharsetUtil.UTF_8); ByteBuf stringFragment = Unpooled.copiedBuffer("\"mutated\"", CharsetUtil.UTF_8); ByteBuf arrayFirstFragment = Unpooled.copiedBuffer("\"first\"", CharsetUtil.UTF_8); SubMultiMutationRequest request = new SubMultiMutationRequest(testInsertionSubKey, bucket(), new MutationCommandBuilder(Mutation.COUNTER, "counter", counterFragment).build(), new MutationCommandBuilder(Mutation.DICT_UPSERT, "sub.value3", stringFragment).build(), new MutationCommandBuilder(Mutation.ARRAY_PUSH_FIRST, "sub.array", arrayFirstFragment).build(), new MutationCommandBuilder(Mutation.DELETE, "some.paht").build() ); MultiMutationResponse response = cluster().<MultiMutationResponse>send(request).toBlocking().single(); assertEquals(ResponseStatus.NOT_EXISTS, response.status()); assertEquals(Unpooled.EMPTY_BUFFER, response.content()); assertEquals(-1, response.firstErrorIndex()); assertEquals(ResponseStatus.FAILURE, response.firstErrorStatus()); assertEquals(0, response.responses().size()); assertEquals(0, stringFragment.refCnt()); assertEquals(0, counterFragment.refCnt()); assertEquals(0, arrayFirstFragment.refCnt()); } /** * Helper method to get a whole document and assert its content against a subdoc mutation. * @param key the document ID. * @param expected the JSON content that is expected. */ private void assertMutation(String key, String expected) { //assert the mutation GetResponse finalState = cluster().<GetResponse>send(new GetRequest(key, bucket())).toBlocking().single(); ReferenceCountUtil.releaseLater(finalState.content()); String actual = finalState.content().toString(CharsetUtil.UTF_8); //work around MB-17143 and generally fact that no order/space guarantees // are made for JSON production by the subdoc API. try { Map actualMap = JSON.readValue(actual, Map.class); Map expectedMap = JSON.readValue(expected, Map.class); assertEquals("Expected mutation on " + key + " was not observed", expectedMap, actualMap); } catch (IOException e) { fail("Failure to compare JSON contents: " + e.toString()); } } /** * Helper method to assert if the mutation metadata is correct. * * Note that if mutation metadata is disabled, null is expected. * * @param token the token to check * @throws Exception */ private void assertValidMetadata(MutationToken token) throws Exception { if (isMutationMetadataEnabled()) { assertNotNull(token); assertTrue(token.sequenceNumber() > 0); assertTrue(token.vbucketUUID() != 0); assertTrue(token.vbucketID() > 0); } else { assertNull(token); } } }