/* * Copyright 2015 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 org.couchbase.mock.subdoc; import com.google.gson.JsonElement; import org.junit.BeforeClass; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.math.BigInteger; import static org.junit.Assert.*; // Tests modelled after subjson/tests/t_ops.cc public class ExecutorTest { private static String rootText; @BeforeClass public static void loadJsonTest() throws IOException { InputStream ss = ExecutorTest.class.getClassLoader().getResourceAsStream("subdoc/big_json.json"); StringBuilder sb = new StringBuilder(); InputStreamReader rr = new InputStreamReader(ss); char[] buffer = new char[4096]; for (;;) { int nRead = rr.read(buffer, 0, buffer.length); if (nRead < 0) { break; } sb.append(buffer, 0, nRead); } rootText = sb.toString(); } private static <T> void assertRaisesPriv(Class<T> exp, String doc, String path, Operation code, String value, boolean mkdirP) throws SubdocException { try { Executor.execute(doc, path, code, value, mkdirP); fail(); } catch (SubdocException ex) { assertTrue(String.format("Expected %s. Got %s", exp.getName(), ex.getClass().getName()), exp.isInstance(ex)); } } private static void assertPathNotFound(String doc, String path, Operation code, String value, boolean isMkdir) throws SubdocException { assertRaisesPriv(PathNotFoundException.class, doc, path, code, value, isMkdir); } private static void assertPathNotFound(String doc, String path) throws SubdocException { assertPathNotFound(doc, path, Operation.GET, null, false); } private static void assertPathExists(String doc, String path) throws SubdocException { assertPathExists(doc, path, null); } private static void assertPathExists(String doc, String path, String exp) throws SubdocException { Result res = Executor.execute(doc, path, Operation.GET); if (exp != null) { assertEquals(exp, res.getMatch().getAsString()); } } private static void assertExistsError(String doc, String path, Operation code, String value) throws SubdocException { assertRaisesPriv(PathExistsException.class, doc, path, code, value, false); } private static void assertMismatchError(String doc, String path, Operation code, String value) throws SubdocException { assertMismatchError(doc, path, code, value, false); } private static void assertMismatchError(String doc, String path, Operation code, String value, boolean isMkdirP) throws SubdocException { assertRaisesPriv(PathMismatchException.class, doc, path, code, value, isMkdirP); } private static void assertCannotInsert(String doc, String path, Operation code, String value) throws SubdocException { assertRaisesPriv(CannotInsertException.class, doc, path, code, value, false); } @Test public void testOperations() throws SubdocException { String doc = rootText; Result res = Executor.execute(doc, "name", Operation.GET); assertNotNull(res.getMatch()); assertEquals("Allagash Brewing", res.getMatch().getAsString()); res = Executor.execute(doc, "name", Operation.EXISTS); res = Executor.execute(doc, "address", Operation.REMOVE); assertNotNull(res.getNewDocument()); doc = res.getNewDocument().toString(); assertPathNotFound(doc, "address"); res = Executor.execute(doc, "address", Operation.DICT_ADD, "\"123 Main St.\""); doc = res.getNewDocument().toString(); res = Executor.execute(doc, "address", Operation.GET); assertNotNull(res); assertEquals("123 Main St.", res.getMatch().getAsString()); res = Executor.execute(doc, "address", Operation.REPLACE, "\"33 Marginal Rd.\""); doc = res.getNewDocument().toString(); res = Executor.execute(doc, "address", Operation.GET); assertEquals("33 Marginal Rd.", res.getMatch().getAsString()); assertPathNotFound(rootText, "foo.bar.baz", Operation.DICT_ADD, "[1,2,3]", false); // Try with P res = Executor.execute(doc, "foo.bar.baz", Operation.DICT_ADD, "[1,2,3]", true); doc = res.getNewDocument().toString(); assertEquals("[1,2,3]", Executor.executeGet(doc, "foo.bar.baz").toString()); } @Test public void testGenericOps() throws SubdocException { String doc = rootText; Result res = Executor.execute(doc, "address[0]", Operation.REMOVE); JsonElement elem; doc = res.getNewDocument().toString(); assertPathNotFound(doc, "address[0]"); res = Executor.execute(doc, "address", Operation.REPLACE, "[\"500 B St.\", \"Anytown\", \"USA\"]"); doc = res.getNewDocument().toString(); elem = Executor.executeGet(doc, "address[2]"); assertEquals("USA", elem.getAsString()); res = Executor.execute(doc, "address[1]", Operation.REPLACE, "\"Sacramento\""); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "address[1]"); assertEquals("Sacramento", elem.getAsString()); } // TODO: testReplaceArrayDeep @Test public void testListOps() throws SubdocException { String doc = "{}"; Result res; JsonElement elem; res = Executor.execute(doc, "array", Operation.DICT_UPSERT, "[]"); doc = res.getNewDocString(); res = Executor.execute(doc, "array", Operation.ARRAY_APPEND, "1"); doc = res.getNewDocString(); assertPathExists(doc, "array[0]", "1"); res = Executor.execute(doc, "array", Operation.ARRAY_PREPEND, "0"); doc = res.getNewDocString(); assertPathExists(doc, "array[0]", "0"); assertPathExists(doc, "array[1]", "1"); res = Executor.execute(doc, "array", Operation.ARRAY_APPEND, "2"); doc = res.getNewDocString(); assertPathExists(doc, "array[2]", "2"); res = Executor.execute(doc, "array", Operation.ARRAY_APPEND, "{\"foo\":\"bar\"}"); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "array[3]"); assertTrue(elem.isJsonObject()); elem = Executor.executeGet(doc, "array[3].foo"); assertEquals("bar", elem.getAsString()); // Test array removal... res = Executor.execute(doc, "array[0]", Operation.REMOVE); assertEquals("0", res.getMatch().getAsString()); doc = res.getNewDocString(); // Should be '1' now assertPathExists(doc, "array[0]", "1"); // "POP" res = Executor.execute(doc, "array[-1]", Operation.REMOVE); assertTrue(res.getMatch().isJsonObject()); doc = res.getNewDocString(); res = Executor.execute(doc, "array[-1]", Operation.REMOVE); assertEquals(2, res.getMatch().getAsInt()); } @Test public void testListOpsPrepend() throws SubdocException { String doc = "{}"; Result res; assertPathNotFound(doc, "array", Operation.ARRAY_PREPEND, "123", false); res = Executor.execute(doc, "array", Operation.ARRAY_PREPEND, "123", true); doc = res.getNewDocString(); assertPathExists(doc, "array[0]", "123"); // Empty the array now res = Executor.execute(doc, "array[0]", Operation.REMOVE); assertEquals(123, res.getMatch().getAsInt()); doc = res.getNewDocString(); assertEquals("{\"array\":[]}", doc); // Prepend the first element res = Executor.execute(doc, "array", Operation.ARRAY_PREPEND, "123"); doc = res.getNewDocString(); assertPathExists(doc, "array[0]", "123"); } @Test public void testArrayMultivalue() throws SubdocException { String doc = "{\"array\":[4,5,6]}"; Result res; JsonElement elem; res = Executor.execute(doc, "array", Operation.ARRAY_PREPEND, "1,2,3"); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "array"); assertEquals("[1,2,3,4,5,6]", elem.toString()); res = Executor.execute(doc, "array", Operation.ARRAY_APPEND, "7,8,9"); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "array"); assertEquals("[1,2,3,4,5,6,7,8,9]", elem.toString()); res = Executor.execute(doc, "array[3]", Operation.ARRAY_INSERT, "-3,-2,-1"); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "array[4]"); assertEquals(-2, elem.getAsInt()); } @Test public void testArrayOpsNested() throws SubdocException { String doc = "[0,[1,[2]],{\"key\":\"val\"}]"; Result res; res = Executor.execute(doc, "[1][1][0]", Operation.REMOVE); doc = res.getNewDocString(); assertEquals("[0,[1,[]],{\"key\":\"val\"}]", doc); res = Executor.execute(doc, "[1][1]", Operation.REMOVE); doc = res.getNewDocString(); assertEquals("[0,[1],{\"key\":\"val\"}]", doc); } @Test public void testArrayDelete() throws SubdocException { String doc = "[1,2]"; Result res; res = Executor.execute(doc, "[0]", Operation.REMOVE); assertEquals("[2]", res.getNewDocString()); res = Executor.execute(doc, "[1]", Operation.REMOVE); assertEquals("[1]", res.getNewDocString()); doc = "[1]"; res = Executor.execute(doc, "[0]", Operation.REMOVE); assertEquals("[]", res.getNewDocString()); res = Executor.execute(doc, "[-1]", Operation.REMOVE); assertEquals("[]", res.getNewDocString()); } @Test public void testDictDelete() throws SubdocException { String doc = "{\"0\": 1,\"1\": 2.0}"; Result res; res = Executor.execute(doc, "0", Operation.REMOVE); doc = res.getNewDocString(); assertPathNotFound(doc, "0"); } @Test public void testUnique() throws SubdocException { String doc = "{}"; Result res; res = Executor.execute(doc, "unique", Operation.ADD_UNIQUE, "\"value\"", true); doc = res.getNewDocString(); assertExistsError(doc, "unique", Operation.ADD_UNIQUE, "\"value\""); res = Executor.execute(doc, "unique", Operation.ADD_UNIQUE, "1"); doc = res.getNewDocString(); res = Executor.execute(doc, "unique", Operation.ADD_UNIQUE, "\"1\""); doc = res.getNewDocString(); assertCannotInsert(doc, "unique", Operation.ADD_UNIQUE, "[]"); assertCannotInsert(doc, "unique", Operation.ADD_UNIQUE, "1,2,3"); // Should succeed Executor.execute(doc, "unique", Operation.ADD_UNIQUE, "null"); // Add a complex object res = Executor.execute(doc, "unique", Operation.ARRAY_APPEND, "[]"); doc = res.getNewDocString(); assertMismatchError(doc, "unique", Operation.ADD_UNIQUE, "123456"); } @Test public void testUniqueTopLevel() throws SubdocException { String doc = "[]"; Result res; res = Executor.execute(doc, "", Operation.ADD_UNIQUE, "0"); doc = res.getNewDocString(); assertExistsError(doc, "", Operation.ADD_UNIQUE, "0"); } @Test public void testNumeric() throws SubdocException { String doc = "{}"; Result res; res = Executor.execute(doc, "counter", Operation.COUNTER, "1", true); assertEquals(1, res.getMatch().getAsInt()); doc = res.getNewDocString(); res = Executor.execute(doc, "counter", Operation.COUNTER, "-101"); assertEquals(-100, res.getMatch().getAsInt()); doc = res.getNewDocString(); res = Executor.execute(doc, "counter", Operation.COUNTER, "1"); assertEquals(-99, res.getMatch().getAsInt()); doc = res.getNewDocString(); // Get it raw res = Executor.execute(doc, "counter", Operation.GET); assertEquals(-99, res.getMatch().getAsInt()); // Try with bigger limits res = Executor.execute(doc, "counter", Operation.COUNTER, Long.toString(Long.MAX_VALUE)); assertEquals(Long.MAX_VALUE-99, res.getMatch().getAsLong()); doc = res.getNewDocString(); res = Executor.execute(doc, "counter", Operation.COUNTER, Long.toString(-(Long.MAX_VALUE-99))); assertEquals(0, res.getMatch().getAsInt()); doc = res.getNewDocString(); // Try with another counter res = Executor.execute(doc, "counter2", Operation.DICT_ADD, "9999999999999999999999999999999"); doc = res.getNewDocString(); res = Executor.execute(doc, "counter2", Operation.GET); assertEquals(new BigInteger("9999999999999999999999999999999"), res.getMatch().getAsBigInteger()); // Try incrementing a number that's too big.. assertRaisesPriv(NumberTooBigException.class, doc, "counter2", Operation.COUNTER, "1", false); res = Executor.execute(doc, "counter3", Operation.DICT_ADD, "3.14"); doc = res.getNewDocString(); assertMismatchError(doc, "counter3", Operation.COUNTER, "1"); doc = "[]"; assertPathNotFound(doc, "[0]", Operation.COUNTER, "1", false); assertPathNotFound(doc, "[0]", Operation.COUNTER, "1", true); res = Executor.execute(doc, "", Operation.ARRAY_APPEND, "-20"); doc = res.getNewDocString(); res = Executor.execute(doc, "[0]", Operation.COUNTER, "1"); assertEquals(-19, res.getMatch().getAsInt()); } @Test public void testBadNumFormat() throws SubdocException { String doc = "{}"; assertRaisesPriv(BadNumberException.class, doc, "pth", Operation.COUNTER, "bad", false); assertRaisesPriv(BadNumberException.class, doc, "pth", Operation.COUNTER, "3.14", false); assertRaisesPriv(BadNumberException.class, doc, "pth", Operation.COUNTER, "-", false); assertRaisesPriv(BadNumberException.class, doc, "pth", Operation.COUNTER, "43f", false); assertRaisesPriv(ZeroDeltaException.class, doc, "pth", Operation.COUNTER, "0", false); } @Test public void testNumericLimits() throws SubdocException { String doc = "{\"counter\":" + Long.toString(Long.MAX_VALUE-1) + "}"; Result res; res = Executor.execute(doc, "counter", Operation.COUNTER, "1"); assertEquals(Long.MAX_VALUE, res.getMatch().getAsLong()); doc = res.getNewDocString(); assertRaisesPriv(DeltaTooBigException.class, doc, "counter", Operation.COUNTER, "2", false); doc = "{\"counter\":" + Long.toString(Long.MIN_VALUE+1) + "}"; res = Executor.execute(doc, "counter", Operation.COUNTER, "-1"); assertEquals(Long.MIN_VALUE, res.getMatch().getAsLong()); doc = res.getNewDocString(); assertRaisesPriv(DeltaTooBigException.class, doc, "counter", Operation.COUNTER, "-2", false); } private static void assertBadDictValue(String value) throws SubdocException{ assertRaisesPriv(CannotInsertException.class, "{}", "a_path", Operation.DICT_ADD, value, true); assertRaisesPriv(CannotInsertException.class, "{}", "a_path", Operation.DICT_ADD, value, false); } @Test public void testValueValidation() throws SubdocException { // Gson seems to accept unquoted keys and values! assertBadDictValue("INVALID"); assertBadDictValue("1,2,3,4"); assertBadDictValue("1,\"k2\":2"); assertBadDictValue("{ \"foo\" }"); assertBadDictValue("{ \"foo\": }"); assertBadDictValue("nul"); assertBadDictValue("2.0.0"); assertBadDictValue("2."); assertBadDictValue("2.0e"); assertBadDictValue("2.0e+"); } @Test public void testNegativeIndex() throws SubdocException { String doc = "[1,2,3,4,5,6]"; Result res; JsonElement elem; elem = Executor.executeGet(doc, "[-1]"); assertEquals(6, elem.getAsInt()); doc = "[1,2,3,[4,5,6,[7,8,9]]]"; elem = Executor.executeGet(doc, "[-1][-1][-1]"); assertEquals(9, elem.getAsInt()); res = Executor.execute(doc, "[-1][-1][-1]", Operation.REMOVE); doc = res.getNewDocString(); // Push new value res = Executor.execute(doc, "[-1][-1]", Operation.ARRAY_APPEND, "10"); doc = res.getNewDocString(); assertPathExists(doc, "[-1][-1][-1]", "10"); // Intermixed paths doc = "{\"k1\": [\"first\", {\"k2\":[6,7,8]},\"last\"] }"; elem = Executor.executeGet(doc, "k1[-1]"); assertEquals("last", elem.getAsString()); elem = Executor.executeGet(doc, "k1[1].k2[-1]"); assertEquals(8, elem.getAsInt()); } @Test public void testRootOps() throws SubdocException { String doc = "[]"; Result res; JsonElement elem; elem = Executor.executeGet(doc, ""); assertEquals("[]", elem.toString()); res = Executor.execute(doc, "", Operation.ARRAY_APPEND, "null"); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "[0]"); assertTrue(elem.isJsonNull()); assertRaisesPriv(CannotInsertException.class, doc, "", Operation.REMOVE, null, false); } @Test public void testMismatch() throws SubdocException { Result res; JsonElement elem; assertMismatchError("{}", "", Operation.ARRAY_APPEND, "null"); assertCannotInsert("[]", "", Operation.DICT_UPSERT, "blah"); assertCannotInsert("[]", "key", Operation.DICT_UPSERT, "blah"); String doc = "[null]"; assertCannotInsert(doc, "", Operation.DICT_UPSERT, "blah"); assertCannotInsert(doc, "key", Operation.DICT_UPSERT, "blah"); assertMismatchError(doc, "foo.bar", Operation.ARRAY_APPEND, "null", true); } @Test public void testWhitespace() throws SubdocException { // This test might be more relevant for the C Json parser itself String doc = "[ 1, 2, 3, 4 ]"; assertPathExists(doc, "[-1]", "4"); } /* // Following disabled because we don't do depth verification @Test public void testTooDeep() throws SubdocException { } @Test public void testTooDeepDict() throws SubdocException { } */ @Test public void testArrayInsert() throws SubdocException { String doc = "[1,2,4,5]"; Result res; JsonElement elem; res = Executor.execute(doc, "[2]", Operation.ARRAY_INSERT, "3"); doc = res.getNewDocString(); assertPathExists(doc, "[2]", "3"); assertEquals(5, res.getNewDocument().getAsJsonArray().size()); // Effective prepend via insert res = Executor.execute(doc, "[0]", Operation.ARRAY_INSERT, "0"); doc = res.getNewDocString(); assertPathExists(doc, "[0]", "0"); // Negative index for INSERT not allowed doc = "[1,2,3,5]"; assertRaisesPriv(InvalidPathException.class, doc, "[-1]", Operation.ARRAY_INSERT, "4", false); // Test out of bounds doc = "[1,2,3]"; assertPathNotFound(doc, "[4]", Operation.ARRAY_INSERT, "null", false); // Not using array syntax (final path component is not an array) assertMismatchError(doc, "[0].anything", Operation.ARRAY_INSERT, "null"); // Try with missing parent. Should fail doc = "{}"; assertPathNotFound(doc, "non_exist[0]", Operation.ARRAY_INSERT, "null", false); doc = "[]"; assertCannotInsert(doc, "[0]", Operation.ARRAY_INSERT, "blah"); doc = "{}"; assertMismatchError(doc, "[0]", Operation.ARRAY_INSERT, "null"); } private void assertEmptyError(Operation op, String path) throws SubdocException { assertRaisesPriv(EmptyValueException.class, "{}", path, op, null, false); } @Test public void testEmpty() throws SubdocException { assertEmptyError(Operation.DICT_ADD, "p"); assertEmptyError(Operation.DICT_UPSERT, "p"); assertEmptyError(Operation.REPLACE, "p"); assertEmptyError(Operation.ARRAY_APPEND, "p"); assertEmptyError(Operation.ARRAY_PREPEND, "p"); assertEmptyError(Operation.ADD_UNIQUE, "p"); assertEmptyError(Operation.ARRAY_INSERT, "p[0]"); } @Test public void testDeleteNestedArray() throws SubdocException { String doc = "[0,[10,20,[100]],{\"key\":\"value\"}]"; Result res; JsonElement elem; elem = Executor.executeGet(doc, "[1]"); assertEquals("[10,20,[100]]", elem.toString()); res = Executor.execute(doc, "[1][2][0]", Operation.REMOVE); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "[1]"); assertEquals("[10,20,[]]", elem.toString()); res = Executor.execute(doc, "[1][2]", Operation.REMOVE); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "[1]"); assertEquals("[10,20]", elem.toString()); res = Executor.execute(doc, "[1]", Operation.REMOVE); doc = res.getNewDocString(); elem = Executor.executeGet(doc, "[1]"); assertEquals("{\"key\":\"value\"}", elem.toString()); } @Test public void testGetCount() throws SubdocException { String doc = "{}"; JsonElement elem; elem = Executor.execute(doc, "", Operation.GET_COUNT).getMatch(); assertEquals(0, elem.getAsInt()); doc = "[]"; elem = Executor.execute(doc, "", Operation.GET_COUNT).getMatch(); assertEquals(0, elem.getAsInt()); doc = "{\"hello\": \"world\"}"; elem = Executor.execute(doc, "", Operation.GET_COUNT).getMatch(); assertEquals(1, elem.getAsInt()); assertEquals("1", elem.toString()); assertRaisesPriv(PathMismatchException.class, doc, "hello", Operation.GET_COUNT, null, false); assertRaisesPriv(PathNotFoundException.class, doc, "nonexist", Operation.GET_COUNT, null, false); } }