diff --git a/pom.xml b/pom.xml index 645063564..0c51ab1fc 100644 --- a/pom.xml +++ b/pom.xml @@ -108,12 +108,6 @@ 0.6 test - - org.codehaus.jackson - jackson-mapper-asl - 1.9.13 - test - org.hamcrest hamcrest-library @@ -121,9 +115,9 @@ test - com.firebase - firebase-token-generator - 2.0.0 + com.cedarsoftware + java-util + 1.26.0 test diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index 25bd42208..edd66a8b7 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -3,6 +3,7 @@ import static com.google.firebase.internal.Base64Utils.decodeUrlSafeNoPadding; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -35,7 +36,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.junit.Assert; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; @@ -55,47 +55,6 @@ public class FirebaseAppTest { @Rule public FirebaseAppRule firebaseAppRule = new FirebaseAppRule(); - @SuppressWarnings("unused") - private static void assertAuthInitialized(FirebaseApp firebaseApp) { - // TODO(depoll): Re-enable once Auth is available. - // assertThat(FirebaseAuth.getInstancesForTest()).containsKey(firebaseApp - // .getPersistenceKey()); - // FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); - // assertThat(firebaseAuth.getFirebaseApp()).isEqualTo(firebaseApp); - } - - private static void assertCrashInitialized() { - // TODO(depoll): Re-enable if Crash becomes available. - // assertThat(FirebaseCrash.isSingletonInitialized()).isTrue(); - } - - private static void assertScionInitialized() { - // TODO(depoll): Re-enable once Scion becomes available. - // assertThat(Scion.getInstanceForTest()).isNotNull(); - } - - @SuppressWarnings("unused") - private static void assertIidInitialized(FirebaseApp firebaseApp) { - // TODO(depoll): Re-enable once IID becomes available. - // FirebaseInstanceId firebaseInstanceID = - // FirebaseInstanceId.getInstancesForTest() - // .get(firebaseApp.getOptions().getApplicationId()); - // assertThat(firebaseInstanceID).isNotNull(); - } - - // TODO(arondeak): reenable persistence. See b/28158809. - // @Test - // public void testGetApps_persistenceEnabled() { - // FirebaseApp app1 = FirebaseApp.initializeApp(mTargetContext, OPTIONS, "app1"); - // FirebaseApp app2 = FirebaseApp.initializeApp(mTargetContext, OPTIONS, "app2"); - // FirebaseApp.clearInstancesForTest(); - // // Sanity check that instances have been cleared. - // assertThat(FirebaseApp.instances).isEmpty(); - // FirebaseApp app3 = FirebaseApp.initializeApp(mTargetContext, OPTIONS, "app3"); - // // We rely on FirebaseApp's equals override, app1 and app2 are different instances. - // assertThat(FirebaseApp.getApps(mTargetContext)).containsExactly(app1, app2, app3); - // } - private static void invokePublicInstanceMethodWithDefaultValues(Object instance, Method method) throws InvocationTargetException, IllegalAccessException { List parameters = new ArrayList<>(method.getParameterTypes().length); @@ -105,12 +64,6 @@ private static void invokePublicInstanceMethodWithDefaultValues(Object instance, method.invoke(instance, parameters.toArray()); } - @Before - public void setUp() { - // Used by Scion internally. - // Scion.testOnlySetDefaultFactory(new ScionFactory(mTargetContext)); - } - @Test(expected = IllegalStateException.class) public void testGetInstancePersistedNotInitialized() { String name = "myApp"; @@ -212,24 +165,16 @@ public void testMissingInit() { } @Test - public void testAuthInitializedForNonDefaultApp() { + public void testApiInitForNonDefaultApp() { FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, "myApp"); - assertAuthInitialized(firebaseApp); - // Measurement is only initialized for the default app. - // TODO(depoll): Re-enable once Scion becomes available. - // assertThat(Scion.getInstanceForTest()).isNull(); + assertFalse(ImplFirebaseTrampolines.isDefaultApp(firebaseApp)); } @Test public void testApiInitForDefaultApp() { // Explicit initialization of FirebaseApp instance. FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS); - assertTrue(ImplFirebaseTrampolines.isDefaultApp(firebaseApp)); - assertAuthInitialized(firebaseApp); - assertCrashInitialized(); - assertIidInitialized(firebaseApp); - assertScionInitialized(); } @Test diff --git a/src/test/java/com/google/firebase/FirebaseOptionsTest.java b/src/test/java/com/google/firebase/FirebaseOptionsTest.java index c325aacc5..dba86d1bd 100644 --- a/src/test/java/com/google/firebase/FirebaseOptionsTest.java +++ b/src/test/java/com/google/firebase/FirebaseOptionsTest.java @@ -52,6 +52,7 @@ public void onSuccess(GoogleCredential googleCredential) { } @Test + @SuppressWarnings("deprecation") public void createOptionsWithServiceAccountSet() throws IOException, InterruptedException { final Semaphore semaphore = new Semaphore(0); FirebaseOptions firebaseOptions = @@ -115,6 +116,7 @@ public void createOptionsWithCredentialMissing() { } @Test(expected = IllegalStateException.class) + @SuppressWarnings("deprecation") public void createOptionsWithServiceAccountAndCredential() { new FirebaseOptions.Builder() .setServiceAccount(ServiceAccount.EDITOR.asStream()) diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index 45f132c67..6f41e7510 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -16,6 +16,7 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.internal.FirebaseCustomAuthToken; +import com.google.firebase.database.MapBuilder; import com.google.firebase.tasks.Tasks; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; @@ -201,7 +202,7 @@ public void testCreateCustomTokenWithDeveloperClaims() throws Exception { FirebaseAuth auth = FirebaseAuth.getInstance(app); String token = - Tasks.await(auth.createCustomToken("user1", ImmutableMap.of("claim", (Object) "value"))); + Tasks.await(auth.createCustomToken("user1", MapBuilder.of("claim", "value"))); FirebaseCustomAuthToken parsedToken = FirebaseCustomAuthToken.parse(new GsonFactory(), token); assertEquals(parsedToken.getPayload().getUid(), "user1"); diff --git a/src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java b/src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java index a74c4f473..0996c7d20 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseCredentialsTest.java @@ -71,7 +71,7 @@ public void defaultCredentialDoesntRefetch() throws Exception { Assert.assertEquals(ACCESS_TOKEN, token); // We should still be able to fetch the token since the certificate is cached - credentialsFile.delete(); + Assert.assertTrue(credentialsFile.delete()); token = Tasks.await( FirebaseCredentials.applicationDefault(transport, Utils.getDefaultJsonFactory()) diff --git a/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java b/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java index df0bd8598..c10c61ea5 100644 --- a/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java +++ b/src/test/java/com/google/firebase/auth/TestOnlyImplFirebaseAuthTrampolines.java @@ -15,8 +15,6 @@ * Provides trampolines into package-private Auth APIs used by components of Firebase * *

This class will not be compiled into the shipping library and can only be used in tests. - * - * @hide */ public final class TestOnlyImplFirebaseAuthTrampolines { diff --git a/src/test/java/com/google/firebase/database/DataSnapshotTest.java b/src/test/java/com/google/firebase/database/DataSnapshotTest.java new file mode 100644 index 000000000..47e54a156 --- /dev/null +++ b/src/test/java/com/google/firebase/database/DataSnapshotTest.java @@ -0,0 +1,77 @@ +package com.google.firebase.database; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.snapshot.IndexedNode; +import com.google.firebase.database.snapshot.Node; +import com.google.firebase.database.snapshot.NodeUtilities; +import java.util.HashMap; +import java.util.Iterator; +import org.junit.AfterClass; +import org.junit.Test; + +public class DataSnapshotTest { + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + private DataSnapshot snapFor(Object data) { + Node node = NodeUtilities.NodeFromJSON(data); + DatabaseReference ref = new DatabaseReference("https://test.firebaseio.com", TestHelpers + .newTestConfig()); + return new DataSnapshot(ref, IndexedNode.from(node)); + } + + @Test + public void testBasicIteration() { + DataSnapshot snap1 = snapFor(null); + + assertFalse(snap1.hasChildren()); + assertFalse(snap1.getChildren().iterator().hasNext()); + + DataSnapshot snap2 = snapFor(1L); + assertFalse(snap2.hasChildren()); + assertFalse(snap2.getChildren().iterator().hasNext()); + + DataSnapshot snap3 = snapFor(MapBuilder.of("a", 1L, "b", 2L)); + assertTrue(snap3.hasChildren()); + Iterator iter = snap3.getChildren().iterator(); + assertTrue(iter.hasNext()); + + String[] children = new String[] {null, null}; + int i = 0; + for (DataSnapshot child : snap3.getChildren()) { + children[i] = child.getKey(); + i++; + } + assertArrayEquals(children, new String[] {"a", "b"}); + } + + @Test + public void testExists() { + DataSnapshot snap; + + snap = snapFor(new HashMap<>()); + assertFalse(snap.exists()); + + snap = snapFor(MapBuilder.of(".priority", 1)); + assertFalse(snap.exists()); + + snap = snapFor(null); + assertFalse(snap.exists()); + + snap = snapFor(true); + assertTrue(snap.exists()); + + snap = snapFor(5); + assertTrue(snap.exists()); + + snap = snapFor(MapBuilder.of("x", 5)); + assertTrue(snap.exists()); + } +} diff --git a/src/test/java/com/google/firebase/database/DeepEquals.java b/src/test/java/com/google/firebase/database/DeepEquals.java deleted file mode 100644 index 065db447e..000000000 --- a/src/test/java/com/google/firebase/database/DeepEquals.java +++ /dev/null @@ -1,450 +0,0 @@ -package com.google.firebase.database; - -//Pulled from https://code.google.com/p/deep-equals/source/browse/deep-equals/java/com -///cedarsoftware/util/DeepEquals.java?r=5 - -import static org.junit.Assert.fail; - -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Deeply compare two (2) objects. This method will call any overridden equals() methods if they - * exist. If not, it will then proceed to do a field-by-field comparison, and when a non-primitive - * field is encountered, recursively continue the deep comparison. When an array is found, it will - * also ensure that the array contents are deeply equal, not requiring the array instance - * (container) to be identical. This method will successfully compare object graphs that have cycles - * (A->B->C->A). There is no need to ever use the Arrays.deepEquals() method as this is a true and - * more effective super set. - * - * @author John DeRegnaucourt - */ -@SuppressWarnings({"unchecked", "rawtypes"}) -public class DeepEquals { - - private static final Map _customEquals = new ConcurrentHashMap<>(); - private static final Map _customHash = new ConcurrentHashMap<>(); - private static final Map> _reflectedFields = new ConcurrentHashMap<>(); - - public static boolean deepEquals(Object a, Object b) { - Set visited = new HashSet<>(); - return deepEquals(a, b, visited); - } - - private static boolean deepEquals(Object a, Object b, Set visited) { - LinkedList stack = new LinkedList<>(); - stack.addFirst(new DualKey(a, b)); - - while (!stack.isEmpty()) { - DualKey dualKey = stack.removeFirst(); - visited.add(dualKey); - - if (dualKey.key1 == null || dualKey.key2 == null) { - if (dualKey.key1 != dualKey.key2) { - return false; - } - continue; - } - - if (!dualKey.key1.getClass().equals(dualKey.key2.getClass()) - && !weakTypeMatch(dualKey.key1, dualKey.key2)) { - return false; - } - - // Handle all [] types. In order to be equal, the arrays must be the same - // length, be of the same type, be in the same order, and all elements within - // the array must be deeply equivalent. - if (dualKey.key1.getClass().isArray()) { - int len = Array.getLength(dualKey.key1); - if (len != Array.getLength(dualKey.key2)) { - return false; - } - - for (int i = 0; i < len; i++) { - DualKey dk = new DualKey(Array.get(dualKey.key1, i), Array.get(dualKey.key2, i)); - if (!visited.contains(dk)) { - stack.addFirst(dk); - } - } - continue; - } - - // Special handle SortedSets because they are fast to compare because their - // elements must be in the same order to be equivalent Sets. - if (dualKey.key1 instanceof SortedSet) { - if (!compareOrdered(dualKey, stack, visited)) { - return false; - } - continue; - } - - // Handled unordered Sets. This is an expensive comparison because order cannot - // be assumed, therefore it runs in O(n^2) when the Sets are the same length. - if (dualKey.key1 instanceof Set) { - if (!compareUnordered((Set) dualKey.key1, (Set) dualKey.key2, visited)) { - return false; - } - continue; - } - - // Check any Collection that is not a Set. In these cases, element order - // matters, therefore this comparison is faster than using unordered comparison. - if (dualKey.key1 instanceof Collection) { - if (!compareOrdered(dualKey, stack, visited)) { - return false; - } - continue; - } - - // Compare two SortedMaps. This takes advantage of the fact that these - // Maps can be compared in O(N) time due to their ordering. - if (dualKey.key1 instanceof SortedMap) { - Map map1 = (Map) dualKey.key1; - Map map2 = (Map) dualKey.key2; - - if (map1.size() != map2.size()) { - return false; - } - - Iterator i1 = map1.entrySet().iterator(); - Iterator i2 = map2.entrySet().iterator(); - - while (i1.hasNext()) { - Map.Entry entry1 = (Map.Entry) i1.next(); - Map.Entry entry2 = (Map.Entry) i2.next(); - - DualKey dk = new DualKey(entry1.getKey(), entry2.getKey()); - if (!visited.contains(dk)) { // Keys must match - stack.addFirst(dk); - } - - dk = new DualKey(entry1.getValue(), entry2.getValue()); - if (!visited.contains(dk)) { // Values must match - stack.addFirst(dk); - } - } - - continue; - } - - // Compare two Unordered Maps. This works in O(N^2) time. - if (dualKey.key1 instanceof Map) { - Map map1 = (Map) dualKey.key1; - Map map2 = (Map) dualKey.key2; - - if (map1.size() != map2.size()) { - return false; - } - - for (Map.Entry entry1 : map1.entrySet()) { - Map.Entry saveEntry2 = null; - for (Map.Entry entry2 : - map2.entrySet()) { // recurse here (yes, that makes this a Stack-based - // implementation - // with partial recursion in the case of Map keys). - if (deepEquals(entry1.getKey(), entry2.getKey(), visited)) { - saveEntry2 = entry2; - break; - } - } - - if (saveEntry2 == null) { - return false; - } - - // Defer value comparisons to future iterations. - DualKey dk = new DualKey(entry1.getValue(), saveEntry2.getValue()); - if (!visited.contains(dk)) { - stack.addFirst(dk); - } - } - - continue; - } - - if (hasCustomEquals(dualKey.key1.getClass())) { - if (!dualKey.key1.equals(dualKey.key2)) { - return false; - } - continue; - } - - Collection fields = getDeepDeclaredFields(dualKey.key1.getClass()); - - for (Field field : fields) { - try { - DualKey dk = new DualKey(field.get(dualKey.key1), field.get(dualKey.key2)); - if (!visited.contains(dk)) { - stack.addFirst(dk); - } - } catch (Exception e) { - continue; - } - } - } - - return true; - } - - public static void assertEquals(Object a, Object b) { - if (!deepEquals(a, b)) { - fail("Values different.\nExpected: " + a + "\nActual: " + b); - } - } - - private static boolean weakTypeMatch(Object a, Object b) { - return (a instanceof Map && b instanceof Map) || (a instanceof List && b instanceof List); - } - - /** - * Compare two Collections that must be same length and in same order. - * - * @param dualKey DualKey represents the two Collections to compare - * @param visited Collection of objects already compared (prevents cycles) - * @param stack add items to compare to the Stack (Stack versus recursion) - * @return boolean false if the Collections are not the same length, otherwise place collection - * items on Stack to be further compared. - */ - private static boolean compareOrdered( - DualKey dualKey, LinkedList stack, Collection visited) { - Collection col1 = (Collection) dualKey.key1; - Collection col2 = (Collection) dualKey.key2; - if (col1.size() != col2.size()) { - return false; - } - - Iterator i1 = col1.iterator(); - Iterator i2 = col2.iterator(); - - while (i1.hasNext()) { - DualKey dk = new DualKey(i1.next(), i2.next()); - if (!visited.contains(dk)) { - stack.addFirst(dk); - } - } - return true; - } - - /** - * Deeply compare the two sets referenced by dualKey. This method attempts to quickly determine - * inequality by length, then hash, and finally does a deepEquals on each element if the two Sets - * passed by the prior tests. - * - * @param col1 Collection one - * @param col2 Collection two - * @param visited Set containing items that have already been compared, so as to prevent cycles. - * @return boolean true if the Sets are deeply equals, false otherwise. - */ - private static boolean compareUnordered(Collection col1, Collection col2, Set visited) { - if (col1.size() != col2.size()) { - return false; - } - - int h1 = deepHashCode(col1); - int h2 = deepHashCode(col2); - if (h1 != h2) { // Faster than deep equals compare (O(n^2) comparison skipped when not - // equal) - return false; - } - - List copy = new ArrayList(col2); - - for (Object element1 : col1) { - int len = copy.size(); - for (int i = 0; - i < len; - i++) { // recurse here (yes, that makes this a Stack-based implementation with - // partial recursion in the case of unordered Sets). - if (deepEquals(element1, copy.get(i), visited)) { - copy.remove(i); // Shrink 2nd Set - break; - } - } - - if (len == copy.size()) { // Nothing found, therefore Collections are not equivalent - return false; - } - } - - return true; - } - - public static boolean hasCustomEquals(Class c) { - Class origClass = c; - if (_customEquals.containsKey(c)) { - return _customEquals.get(c); - } - - while (!Object.class.equals(c)) { - try { - c.getDeclaredMethod("equals", Object.class); - _customEquals.put(origClass, true); - return true; - } catch (Exception ignored) { // - } - c = c.getSuperclass(); - } - _customEquals.put(origClass, false); - return false; - } - - public static int deepHashCode(Object obj) { - Set visited = new HashSet(); - LinkedList stack = new LinkedList<>(); - stack.addFirst(obj); - int hash = 0; - - while (!stack.isEmpty()) { - obj = stack.removeFirst(); - if (obj == null || visited.contains(obj)) { - continue; - } - - visited.add(obj); - - if (obj.getClass().isArray()) { - int len = Array.getLength(obj); - for (int i = 0; i < len; i++) { - stack.addFirst(Array.get(obj, i)); - } - continue; - } - - if (obj instanceof Collection) { - stack.addAll(0, (Collection) obj); - continue; - } - - if (obj instanceof Map) { - stack.addAll(0, ((Map) obj).keySet()); - stack.addAll(0, ((Map) obj).values()); - continue; - } - - if (hasCustomHashCode(obj.getClass())) { // A real hashCode() method exists, call it. - hash += obj.hashCode(); - continue; - } - - Collection fields = getDeepDeclaredFields(obj.getClass()); - for (Field field : fields) { - try { - stack.addFirst(field.get(obj)); - } catch (Exception ignored) { // - } - } - } - return hash; - } - - public static boolean hasCustomHashCode(Class c) { - Class origClass = c; - if (_customHash.containsKey(c)) { - return _customHash.get(c); - } - - while (!Object.class.equals(c)) { - try { - c.getDeclaredMethod("hashCode"); - _customHash.put(origClass, true); - return true; - } catch (Exception ignored) { // - } - c = c.getSuperclass(); - } - _customHash.put(origClass, false); - return false; - } - - /** - * @param c Class instance - * @return Collection of only the fields in the passed in class that would need further processing - * (reference fields). This makes field traversal on a class faster as it does not need to - * continually process known fields like primitives. - */ - public static Collection getDeepDeclaredFields(Class c) { - if (_reflectedFields.containsKey(c)) { - return _reflectedFields.get(c); - } - Collection fields = new ArrayList<>(); - Class curr = c; - - while (curr != null) { - try { - Field[] local; - local = curr.getDeclaredFields(); - - for (Field field : local) { - if (!field.isAccessible()) { - try { - field.setAccessible(true); - } catch (Exception ignored) { // - } - } - - int modifiers = field.getModifiers(); - if (!Modifier.isStatic(modifiers) - && !field.getName().startsWith("this$") - && !Modifier.isTransient( - modifiers)) { // speed up: do not count static fields, not go back up to enclosing - // object in nested case - fields.add(field); - } - } - } catch (ThreadDeath t) { - throw t; - } catch (Throwable ignored) { // - } - - curr = curr.getSuperclass(); - } - _reflectedFields.put(c, fields); - return fields; - } - - private static class DualKey { - - private Object key1; - private Object key2; - - private DualKey() {} - - private DualKey(Object k1, Object k2) { - key1 = k1; - key2 = k2; - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - - if (!(other instanceof DualKey)) { - return false; - } - - DualKey that = (DualKey) other; - return key1 == that.key1 && key2 == that.key2; - } - - @Override - public int hashCode() { - int h1 = key1 != null ? key1.hashCode() : 0; - int h2 = key2 != null ? key2.hashCode() : 0; - return h1 + h2; - } - } -} diff --git a/src/test/java/com/google/firebase/database/EventRecord.java b/src/test/java/com/google/firebase/database/EventRecord.java index 4619528b2..b6dd4763a 100644 --- a/src/test/java/com/google/firebase/database/EventRecord.java +++ b/src/test/java/com/google/firebase/database/EventRecord.java @@ -2,7 +2,6 @@ import com.google.firebase.database.core.view.Event; -/** User: greg Date: 5/28/13 Time: 10:01 AM */ public class EventRecord { private DataSnapshot snapshot; diff --git a/src/test/java/com/google/firebase/database/ListBuilder.java b/src/test/java/com/google/firebase/database/ListBuilder.java deleted file mode 100644 index 9e5d64774..000000000 --- a/src/test/java/com/google/firebase/database/ListBuilder.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.google.firebase.database; - -import java.util.ArrayList; -import java.util.List; - -/** User: greg Date: 5/29/13 Time: 11:45 AM */ -public class ListBuilder { - - private List list = new ArrayList<>(); - - public ListBuilder put(Object o) { - list.add(o); - return this; - } - - public List build() { - return list; - } -} diff --git a/src/test/java/com/google/firebase/database/MapBuilder.java b/src/test/java/com/google/firebase/database/MapBuilder.java index 1fb305d2c..b07b66c7e 100644 --- a/src/test/java/com/google/firebase/database/MapBuilder.java +++ b/src/test/java/com/google/firebase/database/MapBuilder.java @@ -3,7 +3,6 @@ import java.util.HashMap; import java.util.Map; -/** User: greg Date: 5/24/13 Time: 6:52 PM */ public class MapBuilder { private Map map = new HashMap<>(); @@ -16,4 +15,17 @@ public MapBuilder put(String key, Object value) { public Map build() { return map; } + + public static Map of(String k1, Object v1) { + return new MapBuilder().put(k1, v1).build(); + } + + public static Map of(String k1, Object v1, String k2, Object v2) { + return new MapBuilder().put(k1, v1).put(k2, v2).build(); + } + + public static Map of(String k1, Object v1, String k2, Object v2, String k3, + Object v3) { + return new MapBuilder().put(k1, v1).put(k2, v2).put(k3, v3).build(); + } } diff --git a/src/test/java/com/google/firebase/database/MutableDataTest.java b/src/test/java/com/google/firebase/database/MutableDataTest.java index 42b58d99d..82b02024d 100644 --- a/src/test/java/com/google/firebase/database/MutableDataTest.java +++ b/src/test/java/com/google/firebase/database/MutableDataTest.java @@ -10,12 +10,14 @@ import com.google.firebase.database.core.Path; import com.google.firebase.database.snapshot.Node; import com.google.firebase.database.snapshot.NodeUtilities; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; + import org.junit.Test; public class MutableDataTest { @@ -28,7 +30,7 @@ private MutableData dataFor(Object data) { private MutableData dataForPath(Object data, String path) { Node node = NodeUtilities.NodeFromJSON(null); MutableData root = new MutableData(node); - MutableData result = root.child(path.toString()); + MutableData result = root.child(path); result.setValue(data); return result; } @@ -89,9 +91,9 @@ public void iterationWorksAlphaPriorities() { expectedPriorities.addAll( Arrays.asList("fifth", "first", "fourth", "second", "seventh", "sixth", "third")); - DeepEquals.assertEquals(expectedNames, names); - DeepEquals.assertEquals(expectedValues, values); - DeepEquals.assertEquals(expectedPriorities, priorities); + TestHelpers.assertDeepEquals(expectedNames, names); + TestHelpers.assertDeepEquals(expectedValues, values); + TestHelpers.assertDeepEquals(expectedPriorities, priorities); } @Test @@ -108,7 +110,7 @@ public void writingData() { childData.setValue(3); Map expected = new MapBuilder().put("a", 1L).put("b", 3L).build(); - DeepEquals.assertEquals(expected, data.getValue()); + TestHelpers.assertDeepEquals(expected, data.getValue()); int count = 0; for (MutableData unused : data.getChildren()) { @@ -143,7 +145,7 @@ public void mutableDataNavigation() { .put("b", 2L) .put("c", new MapBuilder().put("d", 4L).build()) .build(); - DeepEquals.assertEquals(expected, data.getValue()); + TestHelpers.assertDeepEquals(expected, data.getValue()); } @Test diff --git a/src/test/java/com/google/firebase/database/ObjectMapTest.java b/src/test/java/com/google/firebase/database/ObjectMapTest.java index 144c88827..f1d8e4d53 100644 --- a/src/test/java/com/google/firebase/database/ObjectMapTest.java +++ b/src/test/java/com/google/firebase/database/ObjectMapTest.java @@ -5,11 +5,13 @@ import static org.junit.Assert.fail; import com.google.firebase.database.snapshot.EmptyNode; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; + import org.junit.Test; public class ObjectMapTest { @@ -83,7 +85,7 @@ public void objectToNative() { .put("author", new MapBuilder().put("name", "Greg").put("id", 3L).build()) .build(); - DeepEquals.assertEquals(expected, result); + TestHelpers.assertDeepEquals(expected, result); } @Test @@ -97,7 +99,7 @@ public void nullsWork() { Map result = (Map) data.getValue(); Map expected = new MapBuilder().put("text", "hello world").build(); - DeepEquals.assertEquals(expected, result); + TestHelpers.assertDeepEquals(expected, result); Message resultMessage = data.getValue(Message.class); assertEquals(m, resultMessage); @@ -126,7 +128,7 @@ public void basicGenerics() { GenericTypeIndicator> t = new GenericTypeIndicator>() {}; List result = data.getValue(t); - DeepEquals.assertEquals(authors, result); + TestHelpers.assertDeepEquals(authors, result); } @Test @@ -143,7 +145,7 @@ public void nestedGenerics() throws IOException { AuthorMessages result = data.getValue(AuthorMessages.class); assertEquals(am.getAuthor(), result.getAuthor()); - DeepEquals.assertEquals(am.getMessages(), result.getMessages()); + TestHelpers.assertDeepEquals(am.getMessages(), result.getMessages()); } @Test diff --git a/src/test/java/com/google/firebase/database/PerformanceBenchmarks.java b/src/test/java/com/google/firebase/database/PerformanceBenchmarks.java deleted file mode 100644 index 644f7157a..000000000 --- a/src/test/java/com/google/firebase/database/PerformanceBenchmarks.java +++ /dev/null @@ -1,249 +0,0 @@ -package com.google.firebase.database; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import org.junit.Assert; - -public class PerformanceBenchmarks { - - private static String alphaNumeric = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - private static void addChildren(DatabaseReference reference, int amount, int offset) - throws InterruptedException { - Random random = new Random(); - for (int i = 0; i < amount; i++) { - String randomStr = "p-" + random.nextInt(Integer.MAX_VALUE); - Map map = - new MapBuilder().put("g", randomStr).put("l", Arrays.asList(0, 0)).build(); - reference.child("k-" + (i + offset)).setValue(map, randomStr); - } - } - - private static void addChildrenAndWait(DatabaseReference reference, int amount) - throws InterruptedException { - addChildren(reference, amount, 0); - final Semaphore semaphore = new Semaphore(0); - reference - .child("k-" + amount) - .setValue( - "last-value", - new DatabaseReference.CompletionListener() { - @Override - public void onComplete(DatabaseError error, DatabaseReference ref) { - semaphore.release(); - } - }); - Assert.assertTrue(semaphore.tryAcquire(60, TimeUnit.SECONDS)); - } - - private static String randomString(Random random, String characters, int length) { - char[] text = new char[length]; - for (int i = 0; i < length; i++) { - text[i] = characters.charAt(random.nextInt(characters.length())); - } - return new String(text); - } - - //@Test - public void queryPerformance() throws InterruptedException { - DatabaseReference ref = TestHelpers.getRandomNode(); - - System.err.println("Setting up..."); - - final int initialNumberOfChildren = 20000; - final int furtherNumberOfChildren = 2000; - final int totalNumberOfChildren = initialNumberOfChildren + furtherNumberOfChildren; - - addChildrenAndWait(ref, initialNumberOfChildren); - - System.err.println("Settling down..."); - Thread.sleep(5000); - System.err.println("Benchmarking..."); - - final int[] counter = new int[1]; - final Semaphore semaphore = new Semaphore(0); - final long start = System.currentTimeMillis(); - ChildEventListener listener = - new ChildEventListener() { - @Override - public void onChildAdded(DataSnapshot snapshot, String previousChildName) { - counter[0]++; - if (counter[0] == totalNumberOfChildren) { - semaphore.release(); - } - } - - @Override - public void onChildChanged(DataSnapshot snapshot, String previousChildName) {} - - @Override - public void onChildRemoved(DataSnapshot snapshot) {} - - @Override - public void onChildMoved(DataSnapshot snapshot, String previousChildName) {} - - @Override - public void onCancelled(DatabaseError error) {} - }; - ref.startAt("p").endAt("q").addChildEventListener(listener); - - addChildren(ref, furtherNumberOfChildren, initialNumberOfChildren + 1); - - semaphore.acquire(); - Assert.assertEquals(totalNumberOfChildren, counter[0]); - System.err.println(String.format("Test took %dms", System.currentTimeMillis() - start)); - ref.removeEventListener(listener); - } - - private Object randomObject(int recursionDepth, int recursionFanOut) { - Random random = new Random(); - if (recursionDepth == 0) { - double randomVar = random.nextDouble(); - if (randomVar < 0.25) { - // String - return randomString(random, alphaNumeric, 16); - } else if (randomVar < 0.5) { - // double - return Math.random(); - } else if (randomVar < 0.75) { - // long - return Long.valueOf((long) (Math.random() * Integer.MAX_VALUE)); - } else { - // boolean - return Math.random() > 0.5; - } - } else { - Map object = new HashMap<>(); - for (int i = 0; i < recursionFanOut; i++) { - Object child = randomObject(recursionDepth - 1, recursionFanOut); - object.put(randomString(random, alphaNumeric, random.nextInt(8) + 4), child); - } - return object; - } - } - - //@Test - public void largeValuePerformance() throws InterruptedException { - DatabaseReference ref = TestHelpers.getRandomNode(); - - final int approximateObjectSize = 2 * 1024 * 1024; - final int topLevelChildren = 200; - final int fanOut = 5; - final int childSizeBytes = 24; - - final int recursionDepth = - (int) Math.log(approximateObjectSize / (topLevelChildren * fanOut * childSizeBytes)); - - // create large object - Map map = new HashMap<>(); - Random random = new Random(); - for (int i = 0; i < topLevelChildren; i++) { - map.put( - randomString(random, alphaNumeric, random.nextInt(8) + 2), - randomObject(recursionDepth, fanOut)); - } - - ref.setValue(map); - - System.err.println("Settling down..."); - Thread.sleep(10000); - System.err.println("Adding listeners..."); - final long start = System.currentTimeMillis(); - final Semaphore semaphore = new Semaphore(0); - - ChildEventListener childListener = - new ChildEventListener() { - @Override - public void onChildAdded(DataSnapshot snapshot, String previousChildName) {} - - @Override - public void onChildChanged(DataSnapshot snapshot, String previousChildName) {} - - @Override - public void onChildRemoved(DataSnapshot snapshot) {} - - @Override - public void onChildMoved(DataSnapshot snapshot, String previousChildName) {} - - @Override - public void onCancelled(DatabaseError error) {} - }; - ref.addChildEventListener(childListener); - - ValueEventListener valueListener = - new ValueEventListener() { - @Override - public void onDataChange(DataSnapshot snapshot) { - semaphore.release(); - } - - @Override - public void onCancelled(DatabaseError error) {} - }; - - ref.addValueEventListener(valueListener); - - semaphore.acquire(); - System.err.println(String.format("Benchmark took %dms", System.currentTimeMillis() - start)); - ref.removeEventListener(childListener); - ref.removeEventListener(valueListener); - } - - //@Test - public void childObserverPerformance() throws InterruptedException { - DatabaseReference ref = TestHelpers.getRandomNode(); - - final int numberOfChildren = 50000; - - final int[] counter = new int[1]; - final Semaphore semaphore = new Semaphore(0); - ChildEventListener listener = - new ChildEventListener() { - @Override - public void onChildAdded(DataSnapshot snapshot, String previousChildName) { - counter[0]++; - semaphore.release(); - } - - @Override - public void onChildChanged(DataSnapshot snapshot, String previousChildName) {} - - @Override - public void onChildRemoved(DataSnapshot snapshot) {} - - @Override - public void onChildMoved(DataSnapshot snapshot, String previousChildName) {} - - @Override - public void onCancelled(DatabaseError error) {} - }; - ref.addChildEventListener(listener); - - String[] values = new String[numberOfChildren]; - Random random = new Random(); - for (int i = 0; i < numberOfChildren; i++) { - values[i] = randomString(random, alphaNumeric, random.nextInt(12) + 4); - } - - System.err.println("Settling down..."); - Thread.sleep(10000); - System.err.println("Benchmarking..."); - - final long start = System.currentTimeMillis(); - - for (int i = 0; i < numberOfChildren; i++) { - DatabaseReference child = ref.push(); - child.setValue(values[i]); - } - - semaphore.acquire(numberOfChildren); - Assert.assertEquals(numberOfChildren, counter[0]); - System.err.println(String.format("Benchmark took %dms", System.currentTimeMillis() - start)); - ref.removeEventListener(listener); - } -} diff --git a/src/test/java/com/google/firebase/database/TestConstants.java b/src/test/java/com/google/firebase/database/TestConstants.java deleted file mode 100644 index b4320132e..000000000 --- a/src/test/java/com/google/firebase/database/TestConstants.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.google.firebase.database; - -/** User: greg Date: 5/23/13 Time: 5:32 PM */ -public class TestConstants { - - public static final String TEST_NAMESPACE = "http://tests.fblocal.com:9000"; - public static final String TEST_ALT_NAMESPACE = "http://tests2.fblocal.com:9000"; - public static final String TEST_REPO = "tests"; - public static final String TEST_SERVER = "fblocal.com:9000"; - public static final String TEST_AUTH_SERVER = "http://fblocal.com:12000"; - public static final String TEST_DOMAIN = "fblocal.com"; - public static final String DEFAULT_RULES_STRING = - "{\n \"rules\": {\n \".read\": true,\n \".write\": true\n }\n}"; - - public static final long TEST_TIMEOUT = 7 * 1000; -} diff --git a/src/test/java/com/google/firebase/database/TestFailure.java b/src/test/java/com/google/firebase/database/TestFailure.java index 70e486de0..b494996af 100644 --- a/src/test/java/com/google/firebase/database/TestFailure.java +++ b/src/test/java/com/google/firebase/database/TestFailure.java @@ -1,6 +1,5 @@ package com.google.firebase.database; -/** User: greg Date: 5/30/13 Time: 9:46 AM */ public class TestFailure extends Exception { public TestFailure(Throwable e) { diff --git a/src/test/java/com/google/firebase/database/TestHelpers.java b/src/test/java/com/google/firebase/database/TestHelpers.java index ea2cef655..ce7f8f0e2 100644 --- a/src/test/java/com/google/firebase/database/TestHelpers.java +++ b/src/test/java/com/google/firebase/database/TestHelpers.java @@ -1,27 +1,24 @@ package com.google.firebase.database; -import static org.junit.Assert.assertEquals; +import static com.cedarsoftware.util.DeepEquals.deepEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.auth.FirebaseCredentials; -import com.google.firebase.database.core.Context; import com.google.firebase.database.core.CoreTestHelpers; import com.google.firebase.database.core.DatabaseConfig; import com.google.firebase.database.core.EventTarget; import com.google.firebase.database.core.Path; -import com.google.firebase.database.core.persistence.PersistenceManager; import com.google.firebase.database.core.view.QuerySpec; import com.google.firebase.database.future.WriteFuture; import com.google.firebase.database.snapshot.ChildKey; +import com.google.firebase.database.util.JsonMapper; import com.google.firebase.database.utilities.DefaultRunLoop; import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestUtils; import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Method; -import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -40,81 +37,11 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.CoreProtocolPNames; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.type.TypeReference; public class TestHelpers { - private static List contexts = new ArrayList<>(); - private static String testSecret = null; private static boolean appInitialized = false; - public static void failOnFirstUncaughtException() { - for (Context ctx : contexts) { - TestEventTarget eventTarget = (TestEventTarget) ctx.getEventTarget(); - Throwable t = eventTarget.caughtException.getAndSet(null); - if (t != null) { - t.printStackTrace(); - fail("Found error on event target"); - } - TestRunLoop runLoop = (TestRunLoop) ctx.getRunLoop(); - t = runLoop.caughtException.getAndSet(null); - if (t != null) { - t.printStackTrace(); - fail("Found error on run loop"); - } - } - } - - public static DatabaseConfig getContext(int i) { - ensureContexts(i + 1); - return contexts.get(i); - } - - public static DatabaseReference rootWithNewContext() throws DatabaseException { - return rootWithConfig(newTestConfig()); - } - - public static DatabaseReference rootWithConfig(DatabaseConfig config) { - return new DatabaseReference(TestConstants.TEST_NAMESPACE, config); - } - - public static DatabaseReference getRandomNode() throws DatabaseException { - return getRandomNode(1).get(0); - } - - public static List getRandomNode(int count) throws DatabaseException { - ensureContexts(count); - - List results = new ArrayList<>(count); - String name = null; - for (int i = 0; i < count; ++i) { - DatabaseReference ref = new DatabaseReference(TestConstants.TEST_NAMESPACE, contexts.get(i)); - if (name == null) { - name = ref.push().getKey(); - } - results.add(ref.child(name)); - } - return results; - } - - public static void goOffline(DatabaseConfig cfg) { - DatabaseReference.goOffline(cfg); - } - - public static void goOnline(DatabaseConfig cfg) { - DatabaseReference.goOnline(cfg); - } - public static DatabaseConfig newFrozenTestConfig() { DatabaseConfig cfg = newTestConfig(); CoreTestHelpers.freezeContext(cfg); @@ -122,14 +49,21 @@ public static DatabaseConfig newFrozenTestConfig() { } public static DatabaseConfig newTestConfig() { - TestHelpers.ensureAppInitialized(); + if (!appInitialized) { + appInitialized = true; + FirebaseApp.initializeApp( + new FirebaseOptions.Builder() + .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream())) + .setDatabaseUrl("http://admin-java-sdk.firebaseio.com") + .build()); + } return newTestConfig(FirebaseApp.getInstance()); } - public static DatabaseConfig newTestConfig(FirebaseApp app) { + private static DatabaseConfig newTestConfig(FirebaseApp app) { TestRunLoop runLoop = new TestRunLoop(); DatabaseConfig config = new DatabaseConfig(); - config.setLogLevel(Logger.Level.DEBUG); + config.setLogLevel(Logger.Level.WARN); config.setEventTarget(new TestEventTarget()); config.setRunLoop(runLoop); config.setFirebaseApp(app); @@ -137,60 +71,31 @@ public static DatabaseConfig newTestConfig(FirebaseApp app) { return config; } + public static DatabaseConfig getDatabaseConfig(FirebaseApp app) { + return FirebaseDatabase.getInstance(app).getConfig(); + } + public static ScheduledExecutorService getExecutorService(DatabaseConfig config) { DefaultRunLoop runLoop = (DefaultRunLoop) config.getRunLoop(); return runLoop.getExecutorService(); } - public static void setForcedPersistentCache(Context ctx, PersistenceManager manager) { - try { - Method method = - Context.class.getDeclaredMethod("forcePersistenceManager", PersistenceManager.class); - method.setAccessible(true); - method.invoke(ctx, manager); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - public static void setLogger( DatabaseConfig ctx, com.google.firebase.database.logging.Logger logger) { ctx.setLogger(logger); } - private static void ensureContexts(int count) { - for (int i = contexts.size(); i < count; ++i) { - contexts.add(newTestConfig()); - } - } - - public static String getTestSecret() { - if (testSecret == null) { - try { - InputStream response = - new URL(TestConstants.TEST_NAMESPACE + "/.nsadmin/.json?key=1234").openStream(); - TypeReference> t = new TypeReference>() {}; - Map data = new ObjectMapper().readValue(response, t); - testSecret = (String) ((List) data.get("secrets")).get(0); - } catch (Throwable e) { - fail("Could not get test secret."); - } - } - return testSecret; - } - public static void waitFor(Semaphore semaphore) throws InterruptedException { waitFor(semaphore, 1); } public static void waitFor(Semaphore semaphore, int count) throws InterruptedException { - waitFor(semaphore, count, TestConstants.TEST_TIMEOUT, TimeUnit.MILLISECONDS); + waitFor(semaphore, count, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } public static void waitFor(Semaphore semaphore, int count, long timeout, TimeUnit unit) throws InterruptedException { boolean success = semaphore.tryAcquire(count, timeout, unit); - failOnFirstUncaughtException(); assertTrue("Operation timed out", success); } @@ -215,69 +120,13 @@ public void onCancelled(DatabaseError error) { } }); - semaphore.tryAcquire(1, TestConstants.TEST_TIMEOUT, TimeUnit.MILLISECONDS); + semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); return snapshotList.get(0); } - public static void uploadRules(DatabaseReference ref, String rules) throws IOException { - // TODO: There's some weird flakiness in the rules tests where sometimes most of the tests - // fail in such a way that it *seems* like the rules didn't successfully upload or got - // replaced mid-test. Until we've figured it out, I've added some dumb upload logging. - System.out.println("UPLOADING RULES."); - HttpClient httpClient = new DefaultHttpClient(); - HttpEntity entity = new StringEntity(rules, "UTF-8"); - String url = ref.getRoot().toString() + "/.settings/rules.json?auth=" + getTestSecret(); - HttpPut put = new HttpPut(url); - put.setEntity(entity); - - HttpParams httpParameters = httpClient.getParams(); - httpParameters.setBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false); - HttpConnectionParams.setTcpNoDelay(httpParameters, true); - - HttpResponse response = httpClient.execute(put); - assertTrue(response.getStatusLine().getStatusCode() == 200); - if (rules.length() < 100) { - System.out.println("UPLOADED default rules."); - } else { - System.out.println("UPLOADED custom rules."); - } - } - - public static void assertSuccessfulAuth( - DatabaseReference ref, TestTokenProvider provider, String credential) { - DatabaseReference authRef = ref.getRoot().child(".info/authenticated"); - final Semaphore semaphore = new Semaphore(0); - final List authStates = new ArrayList<>(); - ValueEventListener listener = - authRef.addValueEventListener( - new ValueEventListener() { - @Override - public void onDataChange(DataSnapshot snapshot) { - authStates.add(snapshot.getValue(Boolean.class)); - semaphore.release(); - } - - @Override - public void onCancelled(DatabaseError error) { - throw new RuntimeException("Should not happen"); - } - }); - - provider.setToken(credential); - - try { - TestHelpers.waitFor(semaphore, 2); - assertEquals(Arrays.asList(false, true), authStates); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - authRef.removeEventListener(listener); - } - public static Map fromJsonString(String json) { try { - ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(json, new TypeReference>() {}); + return JsonMapper.parseJson(json); } catch (IOException e) { throw new RuntimeException(e); } @@ -312,38 +161,18 @@ public void run() { } } - public static void waitForEvents(DatabaseReference ref) { - try { - // Make sure queue is done and all events are queued - TestHelpers.waitForQueue(ref); - // Next, all events were queued, make sure all events are done raised - final Semaphore semaphore = new Semaphore(0); - ref.getRepo() - .postEvent( - new Runnable() { - @Override - public void run() { - semaphore.release(); - } - }); - semaphore.acquire(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - public static String repeatedString(String s, int n) { - String result = ""; + StringBuilder result = new StringBuilder(""); for (int i = 0; i < n; i++) { - result += s; + result.append(s); } - return result; + return result.toString(); } - // Create a (test) object which places a test value at then end of the + // Create a (test) object which places a test value at the end of the // object path (e.g., a/b/c would yield {a: {b: {c: "test_value"}}} - public static HashMap buildObjFromPath(Path path, Object testValue) { + public static Map buildObjFromPath(Path path, Object testValue) { final HashMap result = new HashMap<>(); HashMap parent = result; @@ -362,10 +191,9 @@ public static HashMap buildObjFromPath(Path path, Object testVal } // Lookup the value at the path in HashMap (e.g., "a/b/c"). - @SuppressWarnings("unchecked") public static Object applyPath(Object value, Path path) { for (ChildKey key : path) { - value = ((HashMap) value).get(key.asString()); + value = ((Map) value).get(key.asString()); } return value; } @@ -395,21 +223,29 @@ public static Set asSet(T... objects) { } public static Set childKeySet(String... stringKeys) { - HashSet childKeys = new HashSet<>(); + Set childKeys = new HashSet<>(); for (String k : stringKeys) { childKeys.add(ChildKey.fromString(k)); } return childKeys; } - public static void ensureAppInitialized() { - if (!appInitialized) { - appInitialized = true; - FirebaseApp.initializeApp( - new FirebaseOptions.Builder() - .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream())) - .setDatabaseUrl(TestConstants.TEST_NAMESPACE) - .build()); + public static void setHijackHash(DatabaseReference ref, boolean hijackHash) { + ref.setHijackHash(hijackHash); + } + + /** + * Deeply compares two (2) objects. This method will call any overridden equals() methods if they + * exist. If not, it will then proceed to do a field-by-field comparison, and when a non-primitive + * field is encountered, recursively continue the deep comparison. When an array is found, it will + * also ensure that the array contents are deeply equal, not requiring the array instance + * (container) to be identical. This method will successfully compare object graphs that have + * cycles (A->B->C->A). There is no need to ever use the Arrays.deepEquals() method as this is + * a true and more effective super set. + */ + public static void assertDeepEquals(Object a, Object b) { + if (!deepEquals(a, b)) { + fail("Values different.\nExpected: " + a + "\nActual: " + b); } } diff --git a/src/test/java/com/google/firebase/database/TestTokenProvider.java b/src/test/java/com/google/firebase/database/TestTokenProvider.java index f76cfafc6..4c3454221 100644 --- a/src/test/java/com/google/firebase/database/TestTokenProvider.java +++ b/src/test/java/com/google/firebase/database/TestTokenProvider.java @@ -7,7 +7,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -class TestTokenProvider implements AuthTokenProvider { +public class TestTokenProvider implements AuthTokenProvider { private String token; private String nextToken; diff --git a/src/test/java/com/google/firebase/database/ValueExpectationHelper.java b/src/test/java/com/google/firebase/database/ValueExpectationHelper.java index ff848d327..aa65db6f8 100644 --- a/src/test/java/com/google/firebase/database/ValueExpectationHelper.java +++ b/src/test/java/com/google/firebase/database/ValueExpectationHelper.java @@ -2,12 +2,12 @@ import static org.junit.Assert.fail; +import com.cedarsoftware.util.DeepEquals; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.Semaphore; -/** User: greg Date: 6/4/13 Time: 2:45 PM */ public class ValueExpectationHelper { private Semaphore semaphore = new Semaphore(0); diff --git a/src/test/java/com/google/firebase/database/core/RepoInfoTest.java b/src/test/java/com/google/firebase/database/core/RepoInfoTest.java index 36b8e89c2..7501bf271 100644 --- a/src/test/java/com/google/firebase/database/core/RepoInfoTest.java +++ b/src/test/java/com/google/firebase/database/core/RepoInfoTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import com.google.firebase.database.TestConstants; import java.net.URI; import org.junit.Test; @@ -10,14 +9,19 @@ public class RepoInfoTest { @Test public void getConnectionURLTestOverloadWorks() { + final String repo = "tests"; + final String server = "admin-java-sdk.firebaseio.com"; + RepoInfo info = new RepoInfo(); - info.host = TestConstants.TEST_REPO + "." + TestConstants.TEST_SERVER; + info.host = repo + "." + server; info.internalHost = info.host; info.secure = false; - info.namespace = TestConstants.TEST_REPO; + info.namespace = repo; URI url = info.getConnectionURL(null); - assertEquals("ws://tests.fblocal.com:9000/.ws?ns=tests&v=5", url.toString()); + assertEquals("ws://tests.admin-java-sdk.firebaseio.com/.ws?ns=tests&v=5", + url.toString()); url = info.getConnectionURL("test"); - assertEquals("ws://tests.fblocal.com:9000/.ws?ns=tests&v=5&ls=test", url.toString()); + assertEquals("ws://tests.admin-java-sdk.firebaseio.com/.ws?ns=tests&v=5&ls=test", + url.toString()); } } diff --git a/src/test/java/com/google/firebase/database/core/SyncPointTest.java b/src/test/java/com/google/firebase/database/core/SyncPointTest.java index c076cfa38..d4649dba0 100644 --- a/src/test/java/com/google/firebase/database/core/SyncPointTest.java +++ b/src/test/java/com/google/firebase/database/core/SyncPointTest.java @@ -1,5 +1,6 @@ package com.google.firebase.database.core; +import com.google.common.io.CharStreams; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; @@ -20,8 +21,11 @@ import com.google.firebase.database.snapshot.IndexedNode; import com.google.firebase.database.snapshot.Node; import com.google.firebase.database.snapshot.NodeUtilities; +import com.google.firebase.database.util.JsonMapper; +import com.google.firebase.internal.Preconditions; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -29,8 +33,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import org.codehaus.jackson.map.ObjectMapper; -import org.junit.After; + import org.junit.Assert; import org.junit.Test; @@ -46,17 +49,16 @@ public void startListening( Tag tag, ListenHashProvider hash, SyncTree.CompletionListener onListenComplete) { - Path path = query.getPath(); - logger.debug("Listening at " + path + " for Tag " + tag + ")"); - assert !listens.contains(query) : "Duplicate listen"; + Preconditions.checkState(!listens.contains(query), "Duplicate listen"); this.listens.add(query); } @Override public void stopListening(QuerySpec query, Tag tag) { Path path = query.getPath(); - logger.debug("Stop listening at " + path + " for Tag " + tag + ")"); - assert this.listens.contains(query) : "Stopped listening for query already"; + logger.debug("Listening at " + path + " for Tag " + tag); + Preconditions.checkState(this.listens.contains(query), + "Stopped listening for query already"); this.listens.remove(query); } }; @@ -68,9 +70,9 @@ private static EventRegistration getTestEventRegistration(QuerySpec query) { private static void assertEventExactMatch(List expected, List actual) { if (expected.size() < actual.size()) { - Assert.assertTrue("Got extra events: " + actual, false); + Assert.fail("Got extra events: " + actual); } else if (expected.size() > actual.size()) { - Assert.assertTrue("Missing events: " + expected, false); + Assert.fail("Missing events: " + expected); } else { Iterator expectedIterator = expected.iterator(); Iterator actualIterator = actual.iterator(); @@ -275,7 +277,6 @@ private static void runTest(Map testSpec, String basePath) { DatabaseConfig config = TestHelpers.newTestConfig(); TestHelpers.setLogger(config, new DefaultLogger(Logger.Level.WARN, null)); LogWrapper logger = config.getLogger("SyncPointTest"); - logger.info("Running \"" + testSpec.get("name") + '"'); SyncTree.ListenProvider listenProvider = getNewListenProvider(logger); SyncTree syncTree = new SyncTree(config, new NoopPersistenceManager(), listenProvider); @@ -285,9 +286,6 @@ private static void runTest(Map testSpec, String basePath) { List> steps = (List>) testSpec.get("steps"); Map registrations = new HashMap<>(); for (Map spec : steps) { - if (spec.containsKey(".comment")) { - logger.info(" > " + spec.get(".comment")); - } String pathStr = (String) spec.get("path"); Path path = pathStr != null @@ -378,23 +376,15 @@ private static void runTest(Map testSpec, String basePath) { } } - @After - public void tearDown() { - TestHelpers.failOnFirstUncaughtException(); - } - @SuppressWarnings("unchecked") private List> loadSpecs() { - ObjectMapper mapper = new ObjectMapper(); - String pathToResource = "syncPointSpec.json"; - InputStream stream = getClass().getClassLoader().getResourceAsStream(pathToResource); if (stream == null) { throw new RuntimeException("Failed to find syncPointSpec.json resource."); } - try { - return mapper.readValue(stream, List.class); + try (InputStreamReader reader = new InputStreamReader(stream)) { + return (List) JsonMapper.parseJsonValue(CharStreams.toString(reader)); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/com/google/firebase/database/core/SynchronousConnection.java b/src/test/java/com/google/firebase/database/core/SynchronousConnection.java deleted file mode 100644 index c4ba30646..000000000 --- a/src/test/java/com/google/firebase/database/core/SynchronousConnection.java +++ /dev/null @@ -1,231 +0,0 @@ -package com.google.firebase.database.core; - -import static com.google.firebase.database.TestHelpers.newFrozenTestConfig; -import static com.google.firebase.database.utilities.Utilities.hardAssert; - -import com.google.firebase.database.DatabaseError; -import com.google.firebase.database.connection.HostInfo; -import com.google.firebase.database.connection.ListenHashProvider; -import com.google.firebase.database.connection.PersistentConnection; -import com.google.firebase.database.connection.PersistentConnectionImpl; -import com.google.firebase.database.connection.RequestResultCallback; -import com.google.firebase.database.core.view.QuerySpec; -import com.google.firebase.database.snapshot.CompoundHash; -import com.google.firebase.database.snapshot.Node; -import com.google.firebase.database.snapshot.RangeMerge; -import com.google.firebase.database.utilities.DefaultRunLoop; -import com.google.firebase.database.utilities.Pair; -import com.google.firebase.database.utilities.ParsedUrl; -import com.google.firebase.database.utilities.Utilities; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.Semaphore; - -public class SynchronousConnection implements PersistentConnection.Delegate { - - private final Context context; - private final Path path; - private final PersistentConnection connection; - private final Semaphore connectSemaphore; - private Pair> lastRangeMerge; - private boolean destroyed = false; - - public SynchronousConnection(String host) { - ParsedUrl parsed = Utilities.parseUrl(host); - this.path = parsed.path; - context = newFrozenTestConfig(); - ScheduledExecutorService executorService = - ((DefaultRunLoop) context.getRunLoop()).getExecutorService(); - RepoInfo repoInfo = parsed.repoInfo; - HostInfo hostInfo = new HostInfo(repoInfo.host, repoInfo.namespace, repoInfo.secure); - - connection = new PersistentConnectionImpl(context.getConnectionContext(), hostInfo, this); - this.connectSemaphore = new Semaphore(0); - } - - public void connect() { - context - .getRunLoop() - .scheduleNow( - new Runnable() { - @Override - public void run() { - connection.initialize(); - } - }); - try { - this.connectSemaphore.acquire(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - public void destroy() { - destroyed = true; - context - .getRunLoop() - .scheduleNow( - new Runnable() { - @Override - public void run() { - connection.shutdown(); - context.stop(); - } - }); - } - - public void setValue(Path path, final Node node, boolean wait) { - hardAssert(!destroyed); - final Semaphore semaphore = wait ? new Semaphore(0) : null; - final Path fullPath = this.path.child(path); - final DatabaseError[] errorBox = new DatabaseError[1]; - context - .getRunLoop() - .scheduleNow( - new Runnable() { - @Override - public void run() { - connection.put( - fullPath.asList(), - node.getValue(true), - new RequestResultCallback() { - @Override - public void onRequestResult(String optErrorCode, String optErrorMessage) { - errorBox[0] = - (optErrorCode == null) - ? null - : DatabaseError.fromStatus(optErrorCode, optErrorMessage); - if (semaphore != null) { - semaphore.release(); - } - } - }); - } - }); - if (semaphore != null) { - try { - semaphore.acquire(); - if (errorBox[0] != null) { - throw errorBox[0].toException(); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - - public List listenWithHash( - final Path path, final CompoundHash hash, final String simpleHash) { - hardAssert(!destroyed); - lastRangeMerge = null; - final Semaphore semaphore = new Semaphore(0); - final Path fullPath = this.path.child(path); - final QuerySpec spec = QuerySpec.defaultQueryAtPath(fullPath); - context - .getRunLoop() - .scheduleNow( - new Runnable() { - @Override - public void run() { - connection.listen( - spec.getPath().asList(), - spec.getParams().getWireProtocolParams(), - new ListenHashProvider() { - @Override - public com.google.firebase.database.connection.CompoundHash - getCompoundHash() { - List pathPosts = hash.getPosts(); - List> posts = new ArrayList<>(pathPosts.size()); - for (Path path : pathPosts) { - posts.add(path.asList()); - } - return new com.google.firebase.database.connection.CompoundHash( - posts, hash.getHashes()); - } - - @Override - public String getSimpleHash() { - return simpleHash; - } - - @Override - public boolean shouldIncludeCompoundHash() { - return true; - } - }, /*tag=*/ - null, - new RequestResultCallback() { - @Override - public void onRequestResult(String optErrorCode, String optErrorMessage) { - semaphore.release(); - } - }); - } - }); - try { - semaphore.acquire(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - context - .getRunLoop() - .scheduleNow( - new Runnable() { - @Override - public void run() { - connection.unlisten( - spec.getPath().asList(), spec.getParams().getWireProtocolParams()); - } - }); - if (lastRangeMerge == null) { - return null; - } else { - if (!lastRangeMerge.getFirst().equals(path)) { - throw new InternalError("Unexpected path for range merge: " + lastRangeMerge); - } - return lastRangeMerge.getSecond(); - } - } - - @Override - public void onDataUpdate(List path, Object message, boolean isMerge, Long optTag) { - // ignore - } - - @Override - public void onRangeMergeUpdate( - List path, - List merges, - Long optTag) { - List rangeMerges = new ArrayList<>(); - for (com.google.firebase.database.connection.RangeMerge merge : merges) { - rangeMerges.add(new RangeMerge(merge)); - } - - this.lastRangeMerge = new Pair<>(new Path(path), rangeMerges); - } - - @Override - public void onConnect() { - this.connectSemaphore.release(); - } - - @Override - public void onDisconnect() { - if (!destroyed) { - throw new IllegalStateException("Unexpected disconnect!"); - } - } - - @Override - public void onAuthStatus(boolean authOk) { - // ignore - } - - @Override - public void onServerInfoUpdate(Map updates) { - // Ignore - } -} diff --git a/src/test/java/com/google/firebase/database/core/persistence/KeepSyncedTestIT.java b/src/test/java/com/google/firebase/database/core/persistence/KeepSyncedTestIT.java new file mode 100644 index 000000000..6937b9800 --- /dev/null +++ b/src/test/java/com/google/firebase/database/core/persistence/KeepSyncedTestIT.java @@ -0,0 +1,260 @@ +package com.google.firebase.database.core.persistence; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.Query; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.testing.IntegrationTestUtils; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class KeepSyncedTestIT { + + private static long globalKeepSyncedTestCounter = 0; + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() throws IOException { + masterApp = IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testKeepSynced() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ref.keepSynced(true); + assertIsKeptSynced(ref); + + ref.keepSynced(false); + assertNotKeptSynced(ref); + } + + // NOTE: This is not ideal behavior and should be fixed in a future release + @Test + public void testKeepSyncedAffectOnQueries() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ref.keepSynced(true); + Query query = ref.limitToFirst(5); + query.keepSynced(true); + assertIsKeptSynced(ref); + + ref.keepSynced(false); + assertNotKeptSynced(ref); + // currently, setting false on the default query affects all queries at that location + assertNotKeptSynced(query); + } + + @Test + public void testMultipleKeepSynced() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + try { + ref.keepSynced(true); + ref.keepSynced(true); + ref.keepSynced(true); + assertIsKeptSynced(ref); + + // If it were balanced, this would not be enough + ref.keepSynced(false); + ref.keepSynced(false); + assertNotKeptSynced(ref); + + // If it were balanced, this would not be enough + ref.keepSynced(true); + assertIsKeptSynced(ref); + } finally { + // cleanup + ref.keepSynced(false); + } + } + + // NOTE: No RemoveAllListenersDoesNotAffectKeepSynced test, since JVM client doesn't have + // removeAllListeners... + + @Test + public void testRemoveSingleListener() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.keepSynced(true); + try { + assertIsKeptSynced(ref); + + // This will add and remove a listener. + new ReadFuture( + ref, + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + return true; + } + }) + .timedGet(); + + assertIsKeptSynced(ref); + } finally { + // cleanup + ref.keepSynced(false); + } + } + + @Test + public void testKeepSyncedWithExistingListener() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture readFuture; + ref.keepSynced(true); + try { + assertIsKeptSynced(ref); + + readFuture = + new ReadFuture(ref, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + return events.get(events.size() - 1).getSnapshot().getValue().equals("done"); + } + }); + } finally { + // cleanup + ref.keepSynced(false); + } + + // Should trigger our listener. + ref.setValue("done"); + readFuture.timedGet(); + } + + @Test + public void testDifferentIndependentQueries() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + Query query1 = ref.limitToFirst(1); + Query query2 = ref.limitToFirst(2); + + query1.keepSynced(true); + assertIsKeptSynced(query1); + assertNotKeptSynced(query2); + + query2.keepSynced(true); + assertIsKeptSynced(query1); + assertIsKeptSynced(query2); + + query1.keepSynced(false); + assertIsKeptSynced(query2); + assertNotKeptSynced(query1); + + query2.keepSynced(false); + assertNotKeptSynced(query1); + assertNotKeptSynced(query2); + } + + @Test + public void testKeptSyncedChild() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + DatabaseReference child = ref.child("random-child"); + + ref.keepSynced(true); + try { + assertIsKeptSynced(child); + } finally { + // cleanup + ref.keepSynced(false); + } + } + + @Test + public void testKeptSyncedRoot() throws Exception { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); + + ref.keepSynced(true); + try { + assertIsKeptSynced(ref); + } finally { + // cleanup + ref.keepSynced(false); + } + } + + private void assertIsKeptSynced(Query query) throws Exception { + DatabaseReference ref = query.getRef(); + + // First set a unique value to the value of a child. + long counter = globalKeepSyncedTestCounter++; + final Map value = ImmutableMap.of("child", counter); + new WriteFuture(ref, value).timedGet(); + + // Next go offline, if it's kept synced we should have kept the value. + // After going offline no way to get the value except from cache. + ref.getDatabase().goOffline(); + + try { + new ReadFuture( + query, + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + assertEquals(1, events.size()); + assertEquals(value, events.get(0).getSnapshot().getValue()); + return true; + } + }) + .timedGet(); + } finally { + // All good, go back online + ref.getDatabase().goOnline(); + } + } + + private void assertNotKeptSynced(Query query) throws Exception { + DatabaseReference ref = query.getRef(); + + // First set a unique value to the value of a child. + long current = globalKeepSyncedTestCounter++; + final Map oldValue = ImmutableMap.of("child", current); + + long next = globalKeepSyncedTestCounter++; + final Map nextValue = ImmutableMap.of("child", next); + + new WriteFuture(ref, oldValue).timedGet(); + + // Next go offline, if it's kept synced we should have kept the value and we'll get an even + // with the *old* value. + ref.getDatabase().goOffline(); + + try { + ReadFuture readFuture = + new ReadFuture(query, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + // We expect this to get called with the next value, not the old value. + assertEquals(1, events.size()); + assertEquals(nextValue, events.get(0).getSnapshot().getValue()); + return true; + } + }); + + // By now, if we had it synced we should have gotten an event with the wrong value + // Write a new value so the value event listener will be triggered + ref.setValue(nextValue); + readFuture.timedGet(); + } finally { + // All good, go back online + ref.getDatabase().goOnline(); + } + } + + // TODO[offline]: Cancel listens for keep synced.... +} diff --git a/src/test/java/com/google/firebase/database/core/persistence/MockPersistenceStorageEngine.java b/src/test/java/com/google/firebase/database/core/persistence/MockPersistenceStorageEngine.java index afdfc0fb3..e5ddb3ed7 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/MockPersistenceStorageEngine.java +++ b/src/test/java/com/google/firebase/database/core/persistence/MockPersistenceStorageEngine.java @@ -10,6 +10,7 @@ import com.google.firebase.database.snapshot.EmptyNode; import com.google.firebase.database.snapshot.NamedNode; import com.google.firebase.database.snapshot.Node; +import com.google.firebase.database.util.JsonMapper; import com.google.firebase.database.utilities.Utilities; import java.io.IOException; import java.util.ArrayList; @@ -20,7 +21,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.codehaus.jackson.map.ObjectMapper; public class MockPersistenceStorageEngine implements PersistenceStorageEngine { @@ -112,9 +112,8 @@ public void mergeIntoServerCache(Path path, CompoundWrite children) { @Override public long serverCacheEstimatedSizeInBytes() { - final ObjectMapper mapper = new ObjectMapper(); try { - return mapper.writeValueAsString(serverCache.getValue(true)).length(); + return JsonMapper.serializeJson(serverCache.getValue(true)).length(); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/com/google/firebase/database/future/ReadFuture.java b/src/test/java/com/google/firebase/database/future/ReadFuture.java index e8c4567c5..867668992 100644 --- a/src/test/java/com/google/firebase/database/future/ReadFuture.java +++ b/src/test/java/com/google/firebase/database/future/ReadFuture.java @@ -4,11 +4,11 @@ import com.google.firebase.database.DatabaseError; import com.google.firebase.database.EventRecord; import com.google.firebase.database.Query; -import com.google.firebase.database.TestConstants; import com.google.firebase.database.TestFailure; -import com.google.firebase.database.TestHelpers; import com.google.firebase.database.ValueEventListener; import com.google.firebase.database.core.view.Event; +import com.google.firebase.testing.TestUtils; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; @@ -17,7 +17,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -/** User: greg Date: 5/24/13 Time: 11:52 AM */ public class ReadFuture implements Future> { private final ValueEventListener valueEventListener; @@ -167,7 +166,7 @@ public Object waitForLastValue() throws InterruptedException, TimeoutException, } public List timedGet() throws InterruptedException, TimeoutException, TestFailure { - return timedGet(TestConstants.TEST_TIMEOUT, TimeUnit.MILLISECONDS); + return timedGet(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } public List timedGet(long timeout, TimeUnit timeoutUnit) @@ -177,13 +176,12 @@ public List timedGet(long timeout, TimeUnit timeoutUnit) } public void timedWait() throws InterruptedException, TimeoutException, TestFailure { - timedWait(TestConstants.TEST_TIMEOUT, TimeUnit.MILLISECONDS); + timedWait(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } public void timedWait(long timeout, TimeUnit timeoutUnit) throws InterruptedException, TimeoutException, TestFailure { if (!semaphore.tryAcquire(1, timeout, timeoutUnit)) { - TestHelpers.failOnFirstUncaughtException(); throw new TimeoutException(); } if (exception != null) { @@ -197,7 +195,6 @@ private void finish() { } public interface CompletionCondition { - boolean isComplete(List events); } } diff --git a/src/test/java/com/google/firebase/database/future/WriteFuture.java b/src/test/java/com/google/firebase/database/future/WriteFuture.java index a53ed3722..c66ce7c61 100644 --- a/src/test/java/com/google/firebase/database/future/WriteFuture.java +++ b/src/test/java/com/google/firebase/database/future/WriteFuture.java @@ -3,9 +3,9 @@ import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseException; import com.google.firebase.database.DatabaseReference; -import com.google.firebase.database.TestConstants; import com.google.firebase.database.TestFailure; -import com.google.firebase.database.TestHelpers; +import com.google.firebase.testing.TestUtils; + import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; @@ -93,7 +93,6 @@ public DatabaseError get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { boolean success = semaphore.tryAcquire(1, timeout, unit); if (!success) { - TestHelpers.failOnFirstUncaughtException(); throw new TimeoutException(); } return error; @@ -104,6 +103,6 @@ public DatabaseError timedGet() if (error != null) { throw new TestFailure(error.getMessage()); } - return get(TestConstants.TEST_TIMEOUT, TimeUnit.MILLISECONDS); + return get(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } } diff --git a/src/test/java/com/google/firebase/database/integration/DataTestIT.java b/src/test/java/com/google/firebase/database/integration/DataTestIT.java new file mode 100644 index 000000000..297d31b0e --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/DataTestIT.java @@ -0,0 +1,2849 @@ +package com.google.firebase.database.integration; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +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.assertTrue; +import static org.junit.Assert.fail; + +import com.cedarsoftware.util.DeepEquals; +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseException; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.GenericTypeIndicator; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.MutableData; +import com.google.firebase.database.ServerValue; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.Transaction; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.core.DatabaseConfig; +import com.google.firebase.database.core.Path; +import com.google.firebase.database.core.RepoManager; +import com.google.firebase.database.core.view.Event; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.testing.IntegrationTestUtils; +import com.google.firebase.testing.TestUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +public class DataTestIT { + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() { + masterApp = IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testBasicInstantiation() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + assertTrue(ref != null); + } + + @Test + public void testWriteData() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + // just make sure it doesn't throw + ref.setValue(42); + assertTrue(true); + } + + @Test + public void testReadAndWrite() throws InterruptedException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ReadFuture future = ReadFuture.untilNonNull(ref); + + ref.setValue(42); + List events = future.timedGet(); + assertEquals(42L, events.get(events.size() - 1).getSnapshot().getValue()); + } + + @Test + public void testValueReturnsJsonForNodesWithChildren() + throws TimeoutException, InterruptedException, TestFailure { + Map expected = new HashMap<>(); + Map innerExpected = new HashMap<>(); + innerExpected.put("bar", 5L); + expected.put("foo", innerExpected); + + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture future = ReadFuture.untilNonNull(ref); + + ref.setValue(expected); + List events = future.timedGet(); + EventRecord eventRecord = events.get(events.size() - 1); + Object result = eventRecord.getSnapshot().getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testWriteAndWaitForServerConfirmation() + throws TimeoutException, InterruptedException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ref.setValue(42); + + ReadFuture future = new ReadFuture(ref); + + EventRecord eventRecord = future.timedGet().get(0); + assertEquals(42L, eventRecord.getSnapshot().getValue()); + } + + @Test + public void testWriteValueReconnectRead() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + WriteFuture writeFuture = new WriteFuture(writer, 42); + writeFuture.timedGet(); + + ReadFuture future = new ReadFuture(reader); + + EventRecord eventRecord = future.timedGet().get(0); + long result = (Long) eventRecord.getSnapshot().getValue(); + assertEquals(42L, result); + assertEquals(42, (int) eventRecord.getSnapshot().getValue(Integer.class)); + } + + @Test + public void testMultipleWriteValuesReconnectRead() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + writer.child("a").child("b").child("c").setValue(1); + writer.child("a").child("d").child("e").setValue(2); + writer.child("a").child("d").child("f").setValue(3); + WriteFuture writeFuture = new WriteFuture(writer.child("g"), 4); + writeFuture.timedGet(); + + Map expected = new MapBuilder() + .put("a", + new MapBuilder().put("b", new MapBuilder().put("c", 1L).build()) + .put("d", new MapBuilder().put("e", 2L).put("f", 3L).build()).build()) + .put("g", 4L).build(); + ReadFuture readFuture = new ReadFuture(reader); + + GenericTypeIndicator> t = new GenericTypeIndicator>() { + }; + Map result = readFuture.timedGet().get(0).getSnapshot().getValue(t); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testWriteLeafNodeOverwriteParentNodeWaitForEvents() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa")) + .addChildExpectation(ref.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa")) + .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa") + .addValueExpectation(ref.child("a")).startListening(true); + + ref.child("a/aa").setValue(1); + ref.child("a").setValue(new MapBuilder().put("aa", 2).build()); + + assertTrue(helper.waitForEvents()); + helper.cleanup(); + } + + @Test + public void testWriteLeafNodeOverwriteAtParentMultipleTimesWaitForExpectedEvents() + throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final AtomicInteger bbCount = new AtomicInteger(0); + final ValueEventListener listener = ref.child("a/bb") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertNull(snapshot.getValue()); + assertEquals(1, bbCount.incrementAndGet()); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + final EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa")) + .addChildExpectation(ref.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa")) + .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa") + .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa")) + .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa") + .addValueExpectation(ref.child("a")).startListening(true); + + ref.child("a/aa").setValue(1); + ref.child("a").setValue(MapBuilder.of("aa", 2)); + ref.child("a").setValue(MapBuilder.of("aa", 3)); + ref.child("a").setValue(MapBuilder.of("aa", 3)); + + assertTrue(helper.waitForEvents()); + helper.cleanup(); + assertEquals(1, bbCount.get()); + ref.child("a/bb").removeEventListener(listener); + } + + @Test + public void testWriteParentNodeOverwriteAtLeafNodeWaitForEvents() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa")) + .addChildExpectation(ref.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(ref.child("a")).addValueExpectation(ref.child("a/aa")) + .addChildExpectation(ref.child("a"), Event.EventType.CHILD_CHANGED, "aa") + .addValueExpectation(ref.child("a")).startListening(true); + + ref.child("a").setValue(new MapBuilder().put("aa", 2).build()); + ref.child("a/aa").setValue(1); + + assertTrue(helper.waitForEvents()); + helper.cleanup(); + } + + @Test + @Ignore + public void testWriteLeafNodeRemoveParentWaitForEvents() + throws InterruptedException, TimeoutException, TestFailure, ExecutionException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + EventHelper writeHelper = new EventHelper().addValueExpectation(writer.child("a/aa")) + .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(writer.child("a")) + .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "a").addValueExpectation(writer) + .startListening(true); + + WriteFuture w = new WriteFuture(writer.child("a/aa"), 42); + assertTrue(writeHelper.waitForEvents()); + + w.timedGet(); + EventHelper readHelper = new EventHelper().addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "a").addValueExpectation(reader) + .startListening(); + + assertTrue(readHelper.waitForEvents()); + + readHelper.addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader) + .startListening(); + + writeHelper.addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader) + .startListening(); + + writer.child("a").removeValue(); + assertTrue(writeHelper.waitForEvents()); + assertTrue(readHelper.waitForEvents()); + writeHelper.cleanup(); + readHelper.cleanup(); + + // Make sure we actually have null there now + assertNull(TestHelpers.getSnap(reader).getValue()); + assertNull(TestHelpers.getSnap(writer).getValue()); + + ReadFuture readFuture = ReadFuture.untilNonNull(reader); + + ReadFuture writeFuture = ReadFuture.untilNonNull(writer); + + writer.child("a/aa").setValue(3.1415); + + List readerEvents = readFuture.timedGet(); + List writerEvents = writeFuture.timedGet(); + + DataSnapshot readerSnap = readerEvents.get(readerEvents.size() - 1).getSnapshot(); + readerSnap = readerSnap.child("a/aa"); + assertEquals(3.1415, readerSnap.getValue()); + + DataSnapshot writerSnap = writerEvents.get(writerEvents.size() - 1).getSnapshot(); + writerSnap = writerSnap.child("a/aa"); + assertEquals(3.1415, writerSnap.getValue()); + } + + @Test + @Ignore + public void testWriteLeafNodeRemoveLeafNodeWaitForEvents() + throws InterruptedException, TimeoutException, TestFailure, ExecutionException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + EventHelper writeHelper = new EventHelper().addValueExpectation(writer.child("a/aa")) + .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(writer.child("a")) + .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "a").addValueExpectation(writer) + .startListening(true); + + WriteFuture w = new WriteFuture(writer.child("a/aa"), 42); + assertTrue(writeHelper.waitForEvents()); + + w.timedGet(); + EventHelper readHelper = new EventHelper().addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "a").addValueExpectation(reader) + .startListening(); + + assertTrue(readHelper.waitForEvents()); + + readHelper.addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader) + .startListening(); + + writeHelper.addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_REMOVED, "a").addValueExpectation(reader) + .startListening(); + + writer.child("a/aa").removeValue(); + assertTrue(writeHelper.waitForEvents()); + assertTrue(readHelper.waitForEvents()); + writeHelper.cleanup(); + readHelper.cleanup(); + + DataSnapshot readerSnap = TestHelpers.getSnap(reader); + assertNull(readerSnap.getValue()); + + DataSnapshot writerSnap = TestHelpers.getSnap(writer); + assertNull(writerSnap.getValue()); + + readerSnap = TestHelpers.getSnap(reader.child("a/aa")); + assertNull(readerSnap.getValue()); + + writerSnap = TestHelpers.getSnap(writer.child("a/aa")); + assertNull(writerSnap.getValue()); + + ReadFuture readFuture = ReadFuture.untilNonNull(reader); + ReadFuture writeFuture = ReadFuture.untilNonNull(writer); + + writer.child("a/aa").setValue(3.1415); + + final List readerEvents = readFuture.timedGet(); + final List writerEvents = writeFuture.timedGet(); + + readerSnap = readerEvents.get(readerEvents.size() - 1).getSnapshot(); + readerSnap = readerSnap.child("a/aa"); + assertEquals(3.1415, readerSnap.getValue()); + + writerSnap = writerEvents.get(writerEvents.size() - 1).getSnapshot(); + writerSnap = writerSnap.child("a/aa"); + assertEquals(3.1415, writerSnap.getValue()); + } + + @Test + public void testWriteMultipleLeafNodesRemoveOneLeafNodeWaitForEvents() + throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + final EventHelper writeHelper = new EventHelper().addValueExpectation(writer.child("a/aa")) + .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(writer.child("a")) + .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "a").addValueExpectation(writer) + .addChildExpectation(writer.child("a"), Event.EventType.CHILD_ADDED, "bb") + .addValueExpectation(writer.child("a")) + .addChildExpectation(writer, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(writer) + .startListening(true); + + final EventHelper readHelper = new EventHelper().addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "a").addValueExpectation(reader) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_ADDED, "bb") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(reader) + .startListening(true); + + writer.child("a/aa").setValue(42); + writer.child("a/bb").setValue(24); + + assertTrue(writeHelper.waitForEvents()); + assertTrue(readHelper.waitForEvents()); + + readHelper.addValueExpectation(reader.child("a/aa")) + .addChildExpectation(reader.child("a"), Event.EventType.CHILD_REMOVED, "aa") + .addValueExpectation(reader.child("a")) + .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(reader) + .startListening(); + + writeHelper.addValueExpectation(writer.child("a/aa")) + .addChildExpectation(writer.child("a"), Event.EventType.CHILD_REMOVED, "aa") + .addValueExpectation(writer.child("a")) + .addChildExpectation(writer, Event.EventType.CHILD_CHANGED, "a").addValueExpectation(writer) + .startListening(); + + writer.child("a/aa").removeValue(); + assertTrue(writeHelper.waitForEvents()); + assertTrue(readHelper.waitForEvents()); + + DataSnapshot readerSnap = TestHelpers.getSnap(reader); + DataSnapshot writerSnap = TestHelpers.getSnap(writer); + Map expected = new MapBuilder() + .put("a", new MapBuilder().put("bb", 24L).build()).build(); + TestHelpers.assertDeepEquals(expected, readerSnap.getValue()); + TestHelpers.assertDeepEquals(expected, writerSnap.getValue()); + + readHelper.cleanup(); + writeHelper.cleanup(); + } + + @Test + public void testVerifyNodesStartingWithPeriod() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + try { + ref.child(".foo"); + fail("Should fail"); + } catch (DatabaseException e) { + // No-op + } + + try { + ref.child("foo/.foo"); + fail("Should fail"); + } catch (DatabaseException e) { + // No-op + } + } + + // NOTE: skipping test re: writing .keys and .length. Those features will + // require a new client + // anyways + + @Test + public void testNumericKeysGetTurnedIntoArrays() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ref.child("0").setValue("alpha"); + ref.child("1").setValue("bravo"); + ref.child("2").setValue("charlie"); + ref.child("3").setValue("delta"); + ref.child("4").setValue("echo"); + + DataSnapshot snap = TestHelpers.getSnap(ref); + List expected = ImmutableList.of((Object) "alpha", "bravo", "charlie", + "delta", "echo"); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testWriteFullJsonObjectsWithSetAndGetThemBack() + throws TimeoutException, InterruptedException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + Map expected = new MapBuilder() + .put("a", new MapBuilder().put("aa", 5L).put("ab", 3L).build()) + .put("b", + new MapBuilder().put("ba", "hey there!") + .put("bb", MapBuilder.of("bba", false)).build()) + .put("c", ImmutableList.of(0L, new MapBuilder().put("c_1", 4L).build(), "hey", true, false, + "dude")) + .build(); + + ReadFuture readFuture = ReadFuture.untilNonNull(ref); + + ref.setValue(expected); + List events = readFuture.timedGet(); + Object result = events.get(events.size() - 1).getSnapshot().getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + // NOTE: skipping test for value in callback. DataSnapshot is same instance in + // callback as after + // future is complete + + // NOTE: skipping test for passing a value to push. Not applicable. + + @Test + public void testRemoveCallbackIsHit() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + WriteFuture writeFuture = new WriteFuture(ref, 42); + writeFuture.timedGet(); + + ReadFuture readFuture = new ReadFuture(ref, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + return events.get(events.size() - 1).getSnapshot().getValue() == null; + } + }); + + final Semaphore callbackHit = new Semaphore(0); + ref.removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference callbackRef) { + assertEquals(ref, callbackRef); + callbackHit.release(1); + } + }); + + readFuture.timedGet(); + assertTrue(callbackHit.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, MILLISECONDS)); + // We test the value in the completion condition + } + + @Test + public void testRemoveCallbackIsHitForAlreadyRemovedNodes() throws InterruptedException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore callbackHit = new Semaphore(0); + ref.removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference callbackRef) { + assertEquals(ref, callbackRef); + callbackHit.release(1); + } + }); + ref.removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference callbackRef) { + assertEquals(ref, callbackRef); + callbackHit.release(1); + } + }); + + assertTrue( + callbackHit.tryAcquire(2, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); + } + + @Test + public void testUsingNumbersAsKeys() + throws TimeoutException, InterruptedException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ref.child("3024").setValue(5); + ReadFuture future = new ReadFuture(ref); + + List events = future.timedGet(); + assertFalse(events.get(0).getSnapshot().getValue() instanceof List); + } + + @Test + public void testOnceWithCallbackHitsServerToGetData() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 3); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + DatabaseReference reader2 = refs.get(2); + + final Semaphore semaphore = new Semaphore(0); + reader.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals(null, snapshot.getValue()); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Shouldn't happen"); + } + }); + + TestHelpers.waitFor(semaphore); + new WriteFuture(writer, 42).timedGet(); + + reader2.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals(42L, snapshot.getValue()); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Shouldn't happen"); + } + }); + + TestHelpers.waitFor(semaphore); + } + + // NOTE: skipping forEach abort test. Not relevant, we return an iterable that + // can be stopped + // any + // time. + + @Test + public void testSetAndThenListenForValueEvents() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, "cabbage").timedGet(); + EventRecord event = new ReadFuture(ref).timedGet().get(0); + + assertEquals("cabbage", event.getSnapshot().getValue()); + } + + @Test + public void testHasChildren() + throws TimeoutException, InterruptedException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue(new MapBuilder().put("one", 42).put("two", new MapBuilder().put("a", 5).build()) + .put("three", new MapBuilder().put("a", 5).put("b", 6).build()).build()); + final AtomicBoolean removedTwo = new AtomicBoolean(false); + ReadFuture readFuture = new ReadFuture(ref, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (removedTwo.compareAndSet(false, true)) { + // removedTwo did equal false, now equals true + try { + ref.child("two").removeValue(); + } catch (DatabaseException e) { + fail("Should not fail"); + } + } + return events.size() == 2; + } + }); + + List events = readFuture.timedGet(); + DataSnapshot firstSnap = events.get(0).getSnapshot(); + assertEquals(3, firstSnap.getChildrenCount()); + assertEquals(0, firstSnap.child("one").getChildrenCount()); + assertEquals(1, firstSnap.child("two").getChildrenCount()); + assertEquals(2, firstSnap.child("three").getChildrenCount()); + assertEquals(0, firstSnap.child("four").getChildrenCount()); + + DataSnapshot secondSnap = events.get(1).getSnapshot(); + assertEquals(2, secondSnap.getChildrenCount()); + assertEquals(0, secondSnap.child("two").getChildrenCount()); + } + + @Test + public void testSetNodeWithChildrenToPrimitiveValueAndBack() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + final DatabaseReference writer = refs.get(1); + + final Map json = new MapBuilder().put("a", 5L).put("b", 6L).build(); + final long primitive = 76L; + + new WriteFuture(writer, json).timedGet(); + + final AtomicBoolean sawJson = new AtomicBoolean(false); + final AtomicBoolean sawPrimitive = new AtomicBoolean(false); + + ReadFuture readFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (sawJson.compareAndSet(false, true)) { + try { + writer.setValue(primitive); + } catch (DatabaseException e) { + fail("Shouldn't happen: " + e.toString()); + } + } else { + // Saw the json already + if (sawPrimitive.compareAndSet(false, true)) { + try { + writer.setValue(json); + } catch (DatabaseException e) { + fail("Shouldn't happen: " + e.toString()); + } + } + } + return events.size() == 3; + } + }); + + List events = readFuture.timedGet(); + DataSnapshot readSnap = events.get(0).getSnapshot(); + assertTrue(readSnap.hasChildren()); + TestHelpers.assertDeepEquals(json, readSnap.getValue()); + + readSnap = events.get(1).getSnapshot(); + assertFalse(readSnap.hasChildren()); + assertEquals(primitive, readSnap.getValue()); + + readSnap = events.get(2).getSnapshot(); + assertTrue(readSnap.hasChildren()); + TestHelpers.assertDeepEquals(json, readSnap.getValue()); + } + + @Test + public void testWriteLeafNodeRemoveItTryToAddChildToRemovedNode() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference reader = refs.get(0); + final DatabaseReference writer = refs.get(1); + + writer.setValue(5); + writer.removeValue(); + new WriteFuture(writer.child("abc"), 5).timedGet(); + + DataSnapshot snap = new ReadFuture(reader).timedGet().get(0).getSnapshot(); + + assertEquals(5L, ((Map) snap.getValue()).get("abc")); + } + + @Test + public void testListenForValueThenWriteOnNodeWithExistingData() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + new WriteFuture(writer, new MapBuilder().put("a", 5).put("b", 2).build()).timedGet(); + + ReadFuture readFuture = new ReadFuture(reader); + + // Slight race condition. We're banking on this local set being processed + // before the network catches up with the writer's broadcast. + reader.child("a").setValue(10); + + EventRecord event = readFuture.timedGet().get(0); + assertEquals(10L, event.getSnapshot().child("a").getValue()); + } + + @Test + public void testSetPriorityOnNonexistentNode() throws InterruptedException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + ref.setPriority(1, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference callbackRef) { + assertEquals(ref, callbackRef); + assertNotNull(error); + semaphore.release(1); + } + }); + + assertTrue(semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); + } + + @Test + public void testSetPriority() throws InterruptedException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue("hello!"); + final Semaphore semaphore = new Semaphore(0); + ref.setPriority(10, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference callbackRef) { + assertEquals(ref, callbackRef); + assertNull(error); + semaphore.release(1); + } + }); + + assertTrue(semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); + } + + @Test + public void testSetWithPriority() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference ref1 = refs.get(0); + final DatabaseReference ref2 = refs.get(1); + + ReadFuture readFuture = ReadFuture.untilNonNull(ref1); + + new WriteFuture(ref1, "hello", 5).timedGet(); + List result = readFuture.timedGet(); + DataSnapshot snap = result.get(result.size() - 1).getSnapshot(); + assertEquals(5.0, snap.getPriority()); + assertEquals("hello", snap.getValue()); + + result = ReadFuture.untilNonNull(ref2).timedGet(); + snap = result.get(result.size() - 1).getSnapshot(); + assertEquals(5.0, snap.getPriority()); + assertEquals("hello", snap.getValue()); + } + + // NOTE: skipped test of getPriority on snapshot. Tested above + + // NOTE: skipped test of immediately visible priority changes. Tested above + + @Test + public void testSetOverwritesPriorityOfTopLevelNodesAndChildren() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference ref1 = refs.get(0); + final DatabaseReference ref2 = refs.get(1); + + ref1.setValue(new MapBuilder().put("a", 5).build()); + ref1.setPriority(10); + ref1.child("a").setPriority(18); + new WriteFuture(ref1, new MapBuilder().put("a", 7).build()).timedGet(); + + DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot(); + + assertNull(snap.getPriority()); + assertNull(snap.child("a").getPriority()); + } + + @Test + public void testSetWithPriorityOfLeafNodes() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + new WriteFuture(ref1, "testleaf", "992").timedGet(); + + DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot(); + + assertEquals("992", snap.getPriority()); + } + + @Test + public void testSetPriorityOfAnObject() + throws ExecutionException, TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + new WriteFuture(ref1, new MapBuilder().put("a", 5).build(), "991").timedGet(); + + DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot(); + + assertEquals("991", snap.getPriority()); + } + + // NOTE: skipping a test about following setPriority with set, it's tested + // above + + @Test + public void testGetPriority() + throws TimeoutException, InterruptedException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 7); + + ref.setValue("a"); + ref.setValue("b", 5); + ref.setValue("c", "6"); + ref.setValue("d", 7); + ref.setValue(new MapBuilder().put(".value", "e").put(".priority", 8).build()); + ref.setValue(new MapBuilder().put(".value", "f").put(".priority", "8").build()); + ref.setValue(new MapBuilder().put(".value", "g").put(".priority", null).build()); + + List events = readFuture.timedGet(); + assertNull(events.get(0).getSnapshot().getPriority()); + assertEquals(5.0, events.get(1).getSnapshot().getPriority()); + assertEquals("6", events.get(2).getSnapshot().getPriority()); + assertEquals(7.0, events.get(3).getSnapshot().getPriority()); + assertEquals(8.0, events.get(4).getSnapshot().getPriority()); + assertEquals("8", events.get(5).getSnapshot().getPriority()); + assertNull(events.get(6).getSnapshot().getPriority()); + } + + @Test + public void testExportFormatIncludesPriorities() + throws TimeoutException, InterruptedException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + Map expected = new MapBuilder().put("foo", + new MapBuilder() + .put("bar", new MapBuilder().put(".priority", 7.0).put(".value", 5L).build()) + .put(".priority", "hi").build()) + .build(); + ReadFuture readFuture = ReadFuture.untilNonNull(ref); + ref.setValue(expected); + DataSnapshot snap = readFuture.timedGet().get(0).getSnapshot(); + Object result = snap.getValue(true); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testPriorityIsOverwrittenByServerPriority() + throws TimeoutException, InterruptedException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + final DatabaseReference ref2 = refs.get(1); + + ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref1, 2); + + ref1.setValue("hi", 100); + + new ReadFuture(ref2, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + DataSnapshot snap = events.get(events.size() - 1).getSnapshot(); + Object priority = snap.getPriority(); + if (priority != null && priority.equals(100.0)) { + try { + ref2.setValue("whatever"); + } catch (DatabaseException e) { + fail("Shouldn't happen: " + e.toString()); + } + return true; + } + return false; + } + }).timedGet(); + + List events = readFuture.timedGet(); + assertEquals(100.0, events.get(0).getSnapshot().getPriority()); + assertNull(events.get(1).getSnapshot().getPriority()); + } + + @Test + public void testLargeNumericPriorities() + throws TestFailure, TimeoutException, InterruptedException, ExecutionException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + double priority = 1356721306842.0; + + new WriteFuture(ref1, 5, priority).timedGet(); + DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot(); + assertEquals(priority, snap.getPriority()); + } + + @Test + public void testUrlEncodingAndDecoding() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + + DatabaseReference ref = FirebaseDatabase.getInstance(masterApp) + .getReference("/a%b&c@d/space: /non-ascii:ø"); + String result = ref.toString(); + String expected = IntegrationTestUtils.getDatabaseUrl() + + "/a%25b%26c%40d/space%3A%20/non-ascii%3A%C3%B8"; + assertEquals(expected, result); + + String child = "" + new Random().nextInt(100000000); + new WriteFuture(ref.child(child), "testdata").timedGet(); + DataSnapshot snap = TestHelpers.getSnap(ref.child(child)); + assertEquals("testdata", snap.getValue()); + } + + @Test + public void testNameForRootAndNonRootLocations() throws DatabaseException { + DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference(); + assertNull(ref.getKey()); + assertEquals("a", ref.child("a").getKey()); + assertEquals("c", ref.child("b/c").getKey()); + } + + @Test + public void testNameAndRefForSnapshots() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + + DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference(); + // Clear any data there + new WriteFuture(ref, MapBuilder.of("foo", 10)).timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(ref); + assertNull(snap.getKey()); + assertEquals(ref.toString(), snap.getRef().toString()); + DataSnapshot childSnap = snap.child("a"); + assertEquals("a", childSnap.getKey()); + assertEquals(ref.child("a").toString(), childSnap.getRef().toString()); + childSnap = childSnap.child("b/c"); + assertEquals("c", childSnap.getKey()); + assertEquals(ref.child("a/b/c").toString(), childSnap.getRef().toString()); + } + + @Test + public void testParentForRootAndNonRootLocations() throws DatabaseException { + DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference(); + assertNull(ref.getParent()); + DatabaseReference child = ref.child("a"); + assertEquals(ref, child.getParent()); + child = ref.child("a/b/c"); + assertEquals(ref, child.getParent().getParent().getParent()); + } + + @Test + public void testRootForRootAndNonRootLocations() throws DatabaseException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ref = ref.getRoot(); + assertEquals(IntegrationTestUtils.getDatabaseUrl(), ref.toString()); + ref = ref.getRoot(); // Should be a no-op + assertEquals(IntegrationTestUtils.getDatabaseUrl(), ref.toString()); + } + + // NOTE: skip test about child accepting numbers. Not applicable in a + // type-safe language + + // NOTE: skip test on numeric keys. We throw an exception if they aren't + // strings + + @Test + public void testSetChildAndListenAtTheRoot() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + new WriteFuture(ref1.child("foo"), "hi").timedGet(); + DataSnapshot snap = new ReadFuture(ref2).timedGet().get(0).getSnapshot(); + Map expected = MapBuilder.of("foo", "hi"); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testInvalidPaths() throws InterruptedException { + List badPaths = ImmutableList.of(".test", "test.", "fo$o", "[what", "ever]", "ha#sh"); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + DatabaseReference root = FirebaseDatabase.getInstance(masterApp).getReference(); + DataSnapshot snap = TestHelpers.getSnap(ref); + + for (String path : badPaths) { + try { + ref.child(path); + fail("Should not be a valid path: " + path); + } catch (DatabaseException e) { + // No-op, expected + } + + try { + root.child(path); + fail("Should not be a valid path: " + path); + } catch (DatabaseException e) { + // No-op, expected + } + + try { + root.child(IntegrationTestUtils.getDatabaseUrl() + "/tests/" + path); + fail("Should not be a valid path: " + path); + } catch (DatabaseException e) { + // No-op, expected + } + + try { + snap.child(path); + fail("Should not be a valid path: " + path); + } catch (DatabaseException e) { + // No-op, expected + } + + try { + snap.hasChild(path); + fail("Should not be a valid path: " + path); + } catch (DatabaseException e) { + // No-op, expected + } + } + } + + @Test + public void testInvalidKeys() throws DatabaseException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + List badKeys = ImmutableList.of(".test", "test.", "fo$o", "[what", "ever]", "ha#sh", + "/thing", "thi/ing", "thing/", ""); + + List badObjects = new ArrayList<>(); + for (String key : badKeys) { + badObjects.add(MapBuilder.of(key, "test")); + badObjects.add(MapBuilder.of("deeper", MapBuilder.of(key, "test"))); + } + + // Skipping 'push' portion, that api doesn't exist in Java client + + for (Object badObject : badObjects) { + try { + ref.setValue(badObject); + fail("Should not be a valid object: " + badObject); + } catch (DatabaseException e) { + // No-op, expected + } + + try { + ref.onDisconnect().setValue(badObject); + fail("Should not be a valid object: " + badObject); + } catch (DatabaseException e) { + // No-op, expected + } + + // TODO: Skipping transaction portion for now + } + } + + @Test + public void testInvalidUpdates() throws DatabaseException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + List> badUpdates = ImmutableList.of( + new MapBuilder().put("/", "t").put("a", "t").build(), + new MapBuilder().put("a", "t").put("a/b", "t").build(), + new MapBuilder().put("/a", "t").put("a/b", "t").build(), + new MapBuilder().put("/a/b", "t").put("a", "t").build(), + new MapBuilder().put("/a/b", "t").put("/a/b/.priority", 1.0).build(), + new MapBuilder().put("/a/b/.sv", "timestamp").build(), + new MapBuilder().put("/a/b/.value", "t").build(), + new MapBuilder().put("/a/b/.priority", MapBuilder.of("x", "y")).build()); + for (Map badUpdate : badUpdates) { + try { + ref.updateChildren(badUpdate); + fail("Should not be a valid update: " + badUpdate); + } catch (DatabaseException e) { + // No-op, expected + } + + try { + ref.onDisconnect().updateChildren(badUpdate); + fail("Should not be a valid object: " + badUpdate); + } catch (DatabaseException e) { + // No-op, expected + } + } + } + + @Test + public void testAsciiControlCharacters() throws DatabaseException { + DatabaseReference node = IntegrationTestUtils.getRandomNode(masterApp); + // Test all controls characters PLUS 0x7F (127). + for (int i = 0; i <= 32; i++) { + String ch = new String(Character.toChars(i < 32 ? i : 127)); + Map obj = TestHelpers.buildObjFromPath(new Path(ch), "test_value"); + try { + node.setValue(obj); + fail("Ascii control character should not be allowed in path."); + } catch (DatabaseException e) { + // expected + } + } + } + + @Test + @Ignore + // TODO: Stop ignoring this test once JSON parsing has been fixed. + public void testPathKeyLengthLimits() throws TestFailure, TimeoutException, InterruptedException { + final int maxPathLengthBytes = 768; + final int maxPathDepth = 32; + final String fire = new String(Character.toChars(128293)); + final String base = new String(Character.toChars(26594)); + + List goodKeys = ImmutableList.of( + TestHelpers.repeatedString("k", maxPathLengthBytes - 1), + TestHelpers.repeatedString(fire, maxPathLengthBytes / 4 - 1), + TestHelpers.repeatedString(base, maxPathLengthBytes / 3 - 1), + TestHelpers.repeatedString("key/", maxPathDepth - 1) + "key"); + + class BadGroup { + + private String expectedError; + private List keys; + + private BadGroup(String expectedError, List keys) { + this.expectedError = expectedError; + this.keys = keys; + } + } + + List badGroups = ImmutableList.of( + new BadGroup("key path longer than 768 bytes", + ImmutableList.of(TestHelpers.repeatedString("k", maxPathLengthBytes), + TestHelpers.repeatedString(fire, maxPathLengthBytes / 4), + TestHelpers.repeatedString(base, maxPathLengthBytes / 3), + TestHelpers.repeatedString("j", maxPathLengthBytes / 2) + '/' + + TestHelpers.repeatedString("k", maxPathLengthBytes / 2))), + new BadGroup("Path specified exceeds the maximum depth", + Collections.singletonList(TestHelpers.repeatedString("key/", maxPathDepth) + "key"))); + + DatabaseReference node = FirebaseDatabase.getInstance(masterApp).getReference(); + + // Ensure "good keys" work from the root. + for (String key : goodKeys) { + Path path = new Path(key); + Map obj = TestHelpers.buildObjFromPath(path, "test_value"); + node.setValue(obj); + ReadFuture future = ReadFuture.untilNonNull(node); + assertEquals("test_value", TestHelpers.applyPath(future.waitForLastValue(), path)); + + node.child(key).setValue("another_value"); + future = ReadFuture.untilNonNull(node.child(key)); + assertEquals("another_value", future.waitForLastValue()); + + node.updateChildren(obj); + future = ReadFuture.untilNonNull(node); + assertEquals("test_value", TestHelpers.applyPath(future.waitForLastValue(), path)); + } + + // Ensure "good keys" fail when created from child node (relative paths too + // long). + DatabaseReference nodeChild = IntegrationTestUtils.getRandomNode(masterApp); + for (String key : goodKeys) { + Map obj = TestHelpers.buildObjFromPath(new Path(key), "test_value"); + try { + nodeChild.setValue(obj); + fail("Too-long path for setValue should throw exception."); + } catch (DatabaseException e) { + // expected + } + try { + nodeChild.child(key).setValue("another_value"); + fail("Too-long path before setValue should throw exception."); + } catch (DatabaseException e) { + // expected + } + try { + nodeChild.updateChildren(obj); + fail("Too-long path for updateChildren should throw exception."); + } catch (DatabaseException e) { + // expected + } + try { + Map deepUpdate = MapBuilder.of(key, "test_value"); + nodeChild.updateChildren(deepUpdate); + fail("Too-long path in deep update for updateChildren should throw exception."); + } catch (DatabaseException e) { + // expected + } + } + + for (BadGroup badGroup : badGroups) { + for (String key : badGroup.keys) { + Map obj = TestHelpers.buildObjFromPath(new Path(key), "test_value"); + try { + node.setValue(obj); + fail("Expected setValue(bad key) to throw exception: " + key); + } catch (DatabaseException e) { + TestHelpers.assertContains(e.getMessage(), badGroup.expectedError); + } + try { + node.child(key).setValue("another_value"); + fail("Expected child(\"" + key + "\").setValue() to throw exception: " + key); + } catch (DatabaseException e) { + TestHelpers.assertContains(e.getMessage(), badGroup.expectedError); + } + try { + node.updateChildren(obj); + fail("Expected updateChildrean(bad key) to throw exception: " + key); + } catch (DatabaseException e) { + TestHelpers.assertContains(e.getMessage(), badGroup.expectedError); + } + try { + Map deepUpdate = MapBuilder.of(key, "test_value"); + node.updateChildren(deepUpdate); + fail("Expected updateChildrean(bad deep update key) to throw exception: " + key); + } catch (DatabaseException e) { + TestHelpers.assertContains(e.getMessage(), badGroup.expectedError); + } + try { + node.onDisconnect().setValue(obj); + fail("Expected onDisconnect.setValue(bad key) to throw exception: " + key); + } catch (DatabaseException e) { + TestHelpers.assertContains(e.getMessage(), badGroup.expectedError); + } + try { + node.onDisconnect().updateChildren(obj); + fail("Expected onDisconnect.updateChildren(bad key) to throw exception: " + key); + } catch (DatabaseException e) { + TestHelpers.assertContains(e.getMessage(), badGroup.expectedError); + } + try { + Map deepUpdate = MapBuilder.of(key, "test_value"); + node.onDisconnect().updateChildren(deepUpdate); + fail("Expected onDisconnect.updateChildren(bad deep update key) to throw " + "exception: " + + key); + } catch (DatabaseException e) { + TestHelpers.assertContains(e.getMessage(), badGroup.expectedError); + } + } + } + } + + @Test + public void testNamespaceCaseInsensitive() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + + String child = "" + new Random().nextInt(100000000); + DatabaseReference ref1 = FirebaseDatabase.getInstance(masterApp).getReference(child); + + String url = "http://" + IntegrationTestUtils.getProjectId().toUpperCase() + ".firebaseio.com/" + + child; + DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp).getReferenceFromUrl(url); + + new WriteFuture(ref1, "testdata").timedGet(); + DataSnapshot snap = TestHelpers.getSnap(ref2); + assertEquals("testdata", snap.getValue()); + } + + @Test + public void testNamespacesToStringCaseInsensitiveIn() throws DatabaseException { + DatabaseReference ref1 = FirebaseDatabase.getInstance(masterApp).getReference(); + String url = "http://" + IntegrationTestUtils.getProjectId().toUpperCase() + ".firebaseio.com"; + DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp).getReferenceFromUrl(url); + + assertEquals(ref1.toString(), ref2.toString()); + } + + // NOTE: skipping test on re-entrant remove call. Not an issue w/ work queue + // architecture + + @Test + public void testSetNodeWithQuotedKey() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + Map expected = MapBuilder.of("\"herp\"", 1234L); + new WriteFuture(ref, expected).timedGet(); + DataSnapshot snap = TestHelpers.getSnap(ref); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testSetChildWithQuote() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture readFuture = new ReadFuture(ref); + new WriteFuture(ref.child("\""), 1).timedGet(); + DataSnapshot snap = readFuture.timedGet().get(0).getSnapshot(); + assertEquals(1L, snap.child("\"").getValue()); + } + + @Test + public void testEmptyChildrenGetValueEventBeforeParent() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + EventHelper helper = new EventHelper().addValueExpectation(ref.child("a/aa/aaa")) + .addValueExpectation(ref.child("a/aa")).addValueExpectation(ref.child("a")) + .startListening(); + ref.setValue(MapBuilder.of("b", 5)); + + assertTrue(helper.waitForEvents()); + helper.cleanup(); + } + + // NOTE: skipping test for recursive sets. Java does not have reentrant + // firebase api calls + + @Test + public void testOnAfterSetWaitsForLatestData() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + new WriteFuture(ref1, 5).timedGet(); + new WriteFuture(ref2, 42).timedGet(); + + DataSnapshot snap = new ReadFuture(ref1).timedGet().get(0).getSnapshot(); + assertEquals(42L, snap.getValue()); + } + + @Test + public void testOnceWaitsForLatestDataEachTime() + throws InterruptedException, TestFailure, ExecutionException, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + final Semaphore semaphore = new Semaphore(0); + ref1.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertNull(snapshot.getValue()); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + TestHelpers.waitFor(semaphore); + + new WriteFuture(ref2, 5).timedGet(); + ref1.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals(5L, snapshot.getValue()); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + TestHelpers.waitFor(semaphore); + + new WriteFuture(ref2, 42).timedGet(); + + ref1.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals(42L, snapshot.getValue()); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testMemoryFreeingOnUnlistenDoesNotCorruptData() + throws TestFailure, TimeoutException, InterruptedException, ExecutionException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + final Semaphore semaphore = new Semaphore(0); + final AtomicBoolean hasRun = new AtomicBoolean(false); + ValueEventListener listener = ref1.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (hasRun.compareAndSet(false, true)) { + assertNull(snapshot.getValue()); + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not fail"); + } + }); + + TestHelpers.waitFor(semaphore); + + new WriteFuture(ref1, "test").timedGet(); + ref1.removeEventListener(listener); + + DatabaseReference other = IntegrationTestUtils.getRandomNode(masterApp); + new WriteFuture(other, "hello").timedGet(); + ref1.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals("test", snapshot.getValue()); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not fail"); + } + }); + + TestHelpers.waitFor(semaphore); + + ref2.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals("test", snapshot.getValue()); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not fail"); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testUpdateRaisesCorrectLocalEvents() + throws InterruptedException, TestFailure, TimeoutException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 2); + + ref.setValue(new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4).build()); + + EventHelper helper = new EventHelper().addValueExpectation(ref.child("a")) + .addValueExpectation(ref.child("d")) + .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "a") + .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "d").addValueExpectation(ref) + .startListening(true); + + ref.updateChildren(new MapBuilder().put("a", 4).put("d", 1).build()); + helper.waitForEvents(); + List events = readFuture.timedGet(); + helper.cleanup(); + + Map expected = new MapBuilder().put("a", 4L).put("b", 2L).put("c", 3L) + .put("d", 1L).build(); + Object result = events.get(events.size() - 1).getSnapshot().getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testUpdateRaisesCorrectRemoteEvents() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + new WriteFuture(writer, + new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4).build()).timedGet(); + + EventHelper helper = new EventHelper().addValueExpectation(reader.child("a")) + .addValueExpectation(reader.child("d")) + .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "a") + .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "d").addValueExpectation(reader) + .startListening(true); + + writer.updateChildren(new MapBuilder().put("a", 4).put("d", 1).build()); + helper.waitForEvents(); + helper.cleanup(); + + DataSnapshot snap = TestHelpers.getSnap(reader); + Map expected = new MapBuilder().put("a", 4L).put("b", 2L).put("c", 3L) + .put("d", 1L).build(); + Object result = snap.getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testUpdateRaisesChildEventsOnNewListener() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + EventHelper helper = new EventHelper().addValueExpectation(ref.child("a")) + .addValueExpectation(ref.child("d")) + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "a") + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "d") + .addValueExpectation(ref.child("c")).addValueExpectation(ref.child("d")) + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "c") + .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "d").startListening(); + + ref.updateChildren(new MapBuilder().put("a", 11).put("d", 44).build()); + ref.updateChildren(new MapBuilder().put("c", 33).put("d", 45).build()); + helper.waitForEvents(); + helper.cleanup(); + } + + @Test + public void testUpdateAfterSetLeafNodeWorks() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore semaphore = new Semaphore(0); + final Map expected = new MapBuilder().put("a", 1L).put("b", 2L).build(); + + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (DeepEquals.deepEquals(snapshot.getValue(), expected)) { + semaphore.release(); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + ref.setValue(42); + ref.updateChildren(expected); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testUpdateChangesAreStoredCorrectlyInServer() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + new WriteFuture(writer, + new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4).build()).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + writer.updateChildren(MapBuilder.of("a", 42), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + + DataSnapshot snap = TestHelpers.getSnap(reader); + Map expected = new MapBuilder().put("a", 42L).put("b", 2L).put("c", 3L) + .put("d", 4L).build(); + Object result = snap.getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testUpdateAffectPriorityLocally() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 2); + + ref.setValue(new MapBuilder().put("a", 1).put("b", 2).put("c", 3).build(), "testpri"); + ref.updateChildren(MapBuilder.of("a", 4)); + + List events = readFuture.timedGet(); + DataSnapshot snap = events.get(0).getSnapshot(); + assertEquals("testpri", snap.getPriority()); + + snap = events.get(1).getSnapshot(); + assertEquals(4L, snap.child("a").getValue()); + assertEquals("testpri", snap.getPriority()); + } + + @Test + public void testUpdateAffectPriorityRemotely() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + new WriteFuture(writer, new MapBuilder().put("a", 1).put("b", 2).put("c", 3).build(), "testpri") + .timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(reader); + assertEquals("testpri", snap.getPriority()); + + final Semaphore semaphore = new Semaphore(0); + writer.updateChildren(MapBuilder.of("a", 4), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + snap = TestHelpers.getSnap(reader); + assertEquals("testpri", snap.getPriority()); + } + + @Test + public void testUpdateReplacesChildren() throws InterruptedException, TestFailure, + TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + final ReadFuture readFuture = ReadFuture.untilCountAfterNull(writer, 2); + + writer.setValue( + new MapBuilder().put("a", new MapBuilder().put("aa", 1).put("ab", 2).build()).build()); + final Semaphore semaphore = new Semaphore(0); + Map expected = MapBuilder.of("a", MapBuilder.of("aa", 1L)); + writer.updateChildren(expected, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + + DataSnapshot snap = TestHelpers.getSnap(reader); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + + snap = readFuture.timedGet().get(1).getSnapshot(); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testDeepUpdate() throws InterruptedException, TestFailure, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + final ReadFuture readFuture = ReadFuture.untilCount(writer, 2); + + writer.setValue( + new MapBuilder().put("a", new MapBuilder().put("aa", 1).put("ab", 2).build()).build()); + Map expected = new MapBuilder() + .put("a", new MapBuilder().put("aa", 10L).put("ab", 20L).build()).build(); + Map update = new MapBuilder().put("a/aa", 10).put(".priority", 3.0) + .put("a/ab", new MapBuilder().put(".priority", 2.0).put(".value", 20).build()).build(); + + final Semaphore semaphore = new Semaphore(0); + writer.updateChildren(update, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + + DataSnapshot snap = TestHelpers.getSnap(reader); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + assertEquals(3.0, snap.getPriority()); + + snap = readFuture.timedGet().get(1).getSnapshot(); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + assertEquals(3.0, snap.getPriority()); + + snap = TestHelpers.getSnap(reader.child("a/ab")); + assertEquals(2.0, snap.getPriority()); + } + + // NOTE: skipping test of update w/ scalar value. Disallowed by the type + // system + + @Test + public void testUpdateWithNoChanges() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + ref.updateChildren(new HashMap(), new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + } + + // TODO: consider implementing update stress test + // NOTE: skipping update stress test for now + + @Test + public void testUpdateFiresCorrectEventWhenDeletingChild() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final ReadFuture writerFuture = ReadFuture.untilCountAfterNull(writer, 2); + new WriteFuture(writer, new MapBuilder().put("a", 12).put("b", 6).build()).timedGet(); + final Semaphore semaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + semaphore.release(); + } + return events.size() == 2; + } + }); + + TestHelpers.waitFor(semaphore); + + writer.updateChildren(MapBuilder.of("a", null)); + DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot(); + + Map expected = MapBuilder.of("b", 6L); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + + snap = readerFuture.timedGet().get(1).getSnapshot(); + + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testUpdateFiresCorrectEventOnNewChildren() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final Semaphore readerInitializedSemaphore = new Semaphore(0); + final Semaphore writerInitializedSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + writerInitializedSemaphore.release(); + } + return events.size() == 2; + } + }); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + readerInitializedSemaphore.release(); + } + return events.size() == 2; + } + }); + + TestHelpers.waitFor(readerInitializedSemaphore); + TestHelpers.waitFor(writerInitializedSemaphore); + + writer.updateChildren(MapBuilder.of("a", 42)); + DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot(); + + Map expected = MapBuilder.of("a", 42L); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + + snap = readerFuture.timedGet().get(1).getSnapshot(); + + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testUpdateFiresCorrectEventWhenAllChildrenAreDeleted() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final ReadFuture writerFuture = ReadFuture.untilCountAfterNull(writer, 2); + new WriteFuture(writer, MapBuilder.of("a", 12)).timedGet(); + final Semaphore semaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + semaphore.release(); + } + return events.size() == 2; + } + }); + + TestHelpers.waitFor(semaphore); + + writer.updateChildren(MapBuilder.of("a", null)); + DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot(); + + assertNull(snap.getValue()); + + snap = readerFuture.timedGet().get(1).getSnapshot(); + + assertNull(snap.getValue()); + } + + @Test + public void testUpdateFiresCorrectEventOnChangedChildren() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final ReadFuture writerFuture = ReadFuture.untilCountAfterNull(writer, 2); + new WriteFuture(writer, MapBuilder.of("a", 12)).timedGet(); + final Semaphore semaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + semaphore.release(); + } + return events.size() == 2; + } + }); + + TestHelpers.waitFor(semaphore); + + writer.updateChildren(MapBuilder.of("a", 11)); + DataSnapshot snap = writerFuture.timedGet().get(1).getSnapshot(); + + Map expected = MapBuilder.of("a", 11L); + TestHelpers.assertDeepEquals(expected, snap.getValue()); + + snap = readerFuture.timedGet().get(1).getSnapshot(); + + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testPriorityUpdate() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + Map writeValue = new MapBuilder().put("a", 5).put(".priority", "pri1").build(); + + Map updateValue = new MapBuilder().put("a", 6).put(".priority", "pri2") + .put("b", new MapBuilder().put(".priority", "pri3").put("c", 10).build()).build(); + + final Semaphore semaphore = new Semaphore(0); + writer.setValue(writeValue); + writer.updateChildren(updateValue, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(1); + } + }); + TestHelpers.waitFor(semaphore); + + DataSnapshot snap = TestHelpers.getSnap(reader); + assertEquals(6L, snap.child("a").getValue()); + assertEquals("pri2", snap.getPriority()); + assertEquals("pri3", snap.child("b").getPriority()); + assertEquals(10L, snap.child("b/c").getValue()); + } + + // NOTE: skipping test for circular data structures. StackOverflowError is + // thrown, they'll + // see it. + + // NOTE: skipping test for creating a child name 'hasOwnProperty' + + // NOTE: skipping nesting tests. The Java api is async and doesn't do + // reentrant callbacks + + @Test + public void testParentDeleteShadowsChildListeners() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference deleter = refs.get(1); + + String childName = writer.push().getKey(); + + final AtomicBoolean initialized = new AtomicBoolean(false); + final AtomicBoolean called = new AtomicBoolean(false); + final Semaphore semaphore = new Semaphore(0); + final ValueEventListener listener = deleter.child(childName) + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Object val = snapshot.getValue(); + boolean inited = initialized.get(); + if (val == null && inited) { + assertTrue(called.compareAndSet(false, true)); + } else if (!inited && val != null) { + assertTrue(initialized.compareAndSet(false, true)); + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + writer.child(childName).setValue("foo"); + // Make sure we get the data in the listener before we delete it + TestHelpers.waitFor(semaphore); + + deleter.removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + assertTrue(called.get()); + deleter.child(childName).removeEventListener(listener); + } + + @Test + public void testParentDeleteShadowsNonDefaultChildListeners() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference deleter = refs.get(1); + + String childName = writer.push().getKey(); + + final AtomicBoolean queryCalled = new AtomicBoolean(false); + final AtomicBoolean deepChildCalled = new AtomicBoolean(false); + + final ValueEventListener queryListener = deleter.child(childName).startAt(null, "b") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertNull(snapshot.getValue()); + queryCalled.compareAndSet(false, true); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + final ValueEventListener listener = deleter.child(childName).child("a") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertNull(snapshot.getValue()); + deepChildCalled.compareAndSet(false, true); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + writer.child(childName).setValue("foo"); + final Semaphore semaphore = new Semaphore(0); + deleter.removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + assertTrue(queryCalled.get()); + assertTrue(deepChildCalled.get()); + deleter.child(childName).startAt(null, "b").removeEventListener(queryListener); + deleter.child(childName).child("a").removeEventListener(listener); + } + + @Test + public void testServerValuesSetWithPriorityRemoteEvents() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + Object snap = events.get(events.size() - 1).getSnapshot().getValue(); + return snap != null; + } + }); + + // Wait for initial (null) value. + TestHelpers.waitFor(valSemaphore, 1); + + Map initialValues = new MapBuilder() + .put("a", ServerValue.TIMESTAMP) + .put("b", new MapBuilder() + .put(".value", ServerValue.TIMESTAMP) + .put(".priority", ServerValue.TIMESTAMP).build()) + .build(); + + writer.setValue(initialValues, ServerValue.TIMESTAMP, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord readerEventRecord = readerFuture.timedGet().get(1); + DataSnapshot snap = readerEventRecord.getSnapshot(); + assertEquals(snap.child("a").getValue().getClass(), Long.class); + assertEquals(snap.getPriority().getClass(), Double.class); + assertEquals(snap.getPriority(), snap.child("b").getPriority()); + assertEquals(snap.child("a").getValue(), snap.child("b").getValue()); + assert (Math.abs( + System.currentTimeMillis() - Long.parseLong(snap.child("a").getValue().toString())) < 2000); + } + + @Test + public void testServerValuesSetPriorityRemoteEvents() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final AtomicLong priority = new AtomicLong(0); + reader.addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + } + + @Override + public void onCancelled(DatabaseError error) { + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + priority.set(((Double) snapshot.getPriority()).longValue()); + valSemaphore.release(); + } + }); + + Map initialValues = new MapBuilder() + .put("a", 1) + .put("b", new MapBuilder() + .put(".value", 1) + .put(".priority", 1).build()) + .build(); + + writer.setValue(initialValues, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("a").setPriority(ServerValue.TIMESTAMP, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + TestHelpers.waitFor(valSemaphore); + + assert (Math.abs(System.currentTimeMillis() - priority.get()) < 2000); + } + + @Test + @Ignore + public void testServerValuesUpdateRemoteEvents() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 3; + } + }); + + // Wait for initial (null) value. + TestHelpers.waitFor(valSemaphore, 1); + + writer.child("a/b/d").setValue(1, ServerValue.TIMESTAMP, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + Map updatedValue = new MapBuilder() + .put("b", new MapBuilder() + .put("c", ServerValue.TIMESTAMP) + .put("d", ServerValue.TIMESTAMP).build()) + .build(); + + writer.child("a").updateChildren(updatedValue, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord readerEventRecord = readerFuture.timedGet().get(2); + DataSnapshot snap = readerEventRecord.getSnapshot(); + assertEquals(snap.child("a/b/c").getValue().getClass(), Long.class); + assertEquals(snap.child("a/b/d").getValue().getClass(), Long.class); + assert (Math.abs(System.currentTimeMillis() + - Long.parseLong(snap.child("a/b/c").getValue().toString())) < 2000); + } + + @Test + public void testServerValuesSetWithPriorityLocalEvents() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + Object snap = events.get(events.size() - 1).getSnapshot().getValue(); + return snap != null; + } + }); + + // Wait for initial (null) value. + TestHelpers.waitFor(valSemaphore, 1); + + Map initialValues = new MapBuilder() + .put("a", ServerValue.TIMESTAMP) + .put("b", new MapBuilder() + .put(".value", ServerValue.TIMESTAMP) + .put(".priority", ServerValue.TIMESTAMP).build()) + .build(); + + writer.setValue(initialValues, ServerValue.TIMESTAMP, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord readerEventRecord = writerFuture.timedGet().get(1); + DataSnapshot snap = readerEventRecord.getSnapshot(); + assertEquals(snap.child("a").getValue().getClass(), Long.class); + assertEquals(snap.getPriority().getClass(), Double.class); + assertEquals(snap.getPriority(), snap.child("b").getPriority()); + assertEquals(snap.child("a").getValue(), snap.child("b").getValue()); + assert (Math.abs( + System.currentTimeMillis() - Long.parseLong(snap.child("a").getValue().toString())) < 2000); + } + + @Test + public void testServerValuesSetPriorityLocalEvents() throws InterruptedException { + DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final AtomicLong priority = new AtomicLong(0); + writer.addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + } + + @Override + public void onCancelled(DatabaseError error) { + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + priority.set(((Double) snapshot.getPriority()).longValue()); + valSemaphore.release(); + } + }); + + Map initialValues = new MapBuilder() + .put("a", 1) + .put("b", new MapBuilder() + .put(".value", 1) + .put(".priority", 1).build()) + .build(); + + writer.setValue(initialValues, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("a").setPriority(ServerValue.TIMESTAMP, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + TestHelpers.waitFor(valSemaphore); + + assert (Math.abs(System.currentTimeMillis() - priority.get()) < 2000); + } + + @Test + public void testServerValuesUpdateLocalEvents() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 4; + } + }); + + // Wait for initial (null) value. + TestHelpers.waitFor(valSemaphore, 1); + + writer.child("a/b/d").setValue(1, ServerValue.TIMESTAMP, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + Map updatedValue = new MapBuilder() + .put("b", new MapBuilder() + .put("c", ServerValue.TIMESTAMP) + .put("d", ServerValue.TIMESTAMP).build()) + .build(); + + writer.child("a").updateChildren(updatedValue, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord readerEventRecord = writerFuture.timedGet().get(3); + DataSnapshot snap = readerEventRecord.getSnapshot(); + assertEquals(snap.child("a/b/c").getValue().getClass(), Long.class); + assertEquals(snap.child("a/b/d").getValue().getClass(), Long.class); + assert (Math.abs(System.currentTimeMillis() + - Long.parseLong(snap.child("a/b/c").getValue().toString())) < 2000); + } + + @Test + public void testServerValuesTransactionLocalEvents() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore valSemaphore1 = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore1.release(); + return events.size() == 2; + } + }); + + final Semaphore valSemaphore2 = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore2.release(); + return events.size() == 2; + } + }); + + // Wait for local (null) events. + TestHelpers.waitFor(valSemaphore1); + TestHelpers.waitFor(valSemaphore2); + + writer.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + currentData.setValue(ServerValue.TIMESTAMP); + } catch (DatabaseException e) { + fail("Should not fail"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + if (error != null || !committed) { + fail("Transaction should succeed"); + } + } + }); + + // Wait for local events. + TestHelpers.waitFor(valSemaphore1); + TestHelpers.waitFor(valSemaphore2); + + EventRecord writerEventRecord = writerFuture.timedGet().get(1); + DataSnapshot snap1 = writerEventRecord.getSnapshot(); + assertEquals(snap1.getValue().getClass(), Long.class); + assert (Math + .abs(System.currentTimeMillis() - Long.parseLong(snap1.getValue().toString())) < 2000); + + EventRecord readerEventRecord = readerFuture.timedGet().get(1); + DataSnapshot snap2 = readerEventRecord.getSnapshot(); + assertEquals(snap2.getValue().getClass(), Long.class); + assert (Math + .abs(System.currentTimeMillis() - Long.parseLong(snap2.getValue().toString())) < 2000); + } + + @Test + public void testUpdateAfterChildSet() throws InterruptedException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore doneSemaphore = new Semaphore(0); + + Map initial = MapBuilder.of("a", "a"); + ref.setValue(initial, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Map res = (Map) snapshot.getValue(); + if (res != null && res.size() == 3 && res.containsKey("a") && res.containsKey("b") + && res.containsKey("c")) { + doneSemaphore.release(); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.child("b").setValue("b"); + + Map update = MapBuilder.of("c", "c"); + ref.updateChildren(update); + } + }); + + TestHelpers.waitFor(doneSemaphore); + } + + @Test + public void testMaxFrameSize() + throws InterruptedException, TestFailure, ExecutionException, TimeoutException { + StringBuilder msg = new StringBuilder(""); + // This will generate a string well over max frame size + for (int i = 0; i < 16384; ++i) { + msg.append(String.valueOf(i)); + } + + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, msg.toString()).timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(ref); + assertEquals(msg.toString(), snap.getValue()); + } + + @Test + public void testDeltaSyncNoDataUpdatesAfterReconnect() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + // Create a fresh connection so we can be sure we won't get any other data + // updates for stuff. + final DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp) + .getReference(ref.getKey()); + + final Map data = new MapBuilder().put("a", 1L).put("b", 2L) + .put("c", new MapBuilder().put(".value", 3L).put(".priority", 3.0).build()).put("d", 4L) + .build(); + + final Semaphore gotData = new Semaphore(0); + final AtomicReference result = new AtomicReference<>(); + ref.setValue(data, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref2.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertFalse(gotData.tryAcquire()); + result.compareAndSet(null, (Map) snapshot.getValue(true)); + gotData.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + } + }); + + TestHelpers.waitFor(gotData); + TestHelpers.assertDeepEquals(data, result.get()); + + final Semaphore done = new Semaphore(0); + + // Bounce connection. + DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.interrupt(ctx); + RepoManager.resume(ctx); + + ref2.getRoot().child(".info/connected").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if ((Boolean) snapshot.getValue()) { + // We're connected. Do one more round-trip to make + // sure all state restoration is done + ref2.getRoot().child("foobar/empty/blah").setValue(null, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + done.release(); + } + }); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testDeltaSyncWithQueryNoDataUpdatesAfterReconnect() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + // Create a fresh connection so we can be sure we won't get any other data + // updates for stuff. + final DatabaseReference ref2 = FirebaseDatabase.getInstance(masterApp) + .getReference(ref.getKey()); + + final Map data = new MapBuilder() + .put("a", 1L) + .put("b", 2L) + .put("c", new MapBuilder().put(".value", 3L).put(".priority", 3.0).build()) + .put("d", 4L) + .build(); + + final Map expected = new MapBuilder() + .put("c", new MapBuilder().put(".value", 3L).put(".priority", 3.0).build()) + .put("d", 4L) + .build(); + + final Semaphore gotData = new Semaphore(0); + + final AtomicInteger updateCount = new AtomicInteger(0); + final AtomicReference result = new AtomicReference<>(); + ref.setValue(data, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref2.limitToLast(2).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertFalse(gotData.tryAcquire()); + updateCount.incrementAndGet(); + result.compareAndSet(null, (Map) snapshot.getValue(true)); + gotData.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + } + }); + + TestHelpers.waitFor(gotData); + assertEquals(1, updateCount.get()); + TestHelpers.assertDeepEquals(expected, result.get()); + + final Semaphore done = new Semaphore(0); + + // Bounce connection. + DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.interrupt(ctx); + RepoManager.resume(ctx); + + ref2.getRoot().child(".info/connected").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if ((Boolean) snapshot.getValue()) { + // We're connected. Do one more round-trip to make + // sure all state restoration is // done. + ref2.getRoot().child("foobar/empty/blah").setValue(null, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + done.release(); + } + }); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(done); + } + + @Test + @Ignore + public void testDeltaSyncWithUnfilteredQuery() + throws InterruptedException, ExecutionException, TestFailure, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writeRef = refs.get(0); + DatabaseReference readRef = refs.get(1); + + // List must be large enough to trigger delta sync. + Map longList = new HashMap<>(); + for (long i = 0; i < 50; i++) { + String key = writeRef.push().getKey(); + longList.put(key, + new MapBuilder().put("order", i).put("text", "This is an awesome message!").build()); + } + + new WriteFuture(writeRef, longList).timedGet(); + + // start listening. + final List readSnapshots = new ArrayList<>(); + final Semaphore readSemaphore = new Semaphore(0); + readRef.orderByChild("order").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + readSnapshots.add(snapshot); + readSemaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(readSemaphore); + TestHelpers.assertDeepEquals(longList, readSnapshots.get(0).getValue()); + + // Add a new child while readRef is offline. + readRef.getDatabase().goOffline(); + + DatabaseReference newChildRef = writeRef.push(); + Map newChild = new MapBuilder().put("order", 50L) + .put("text", "This is a new child!").build(); + new WriteFuture(newChildRef, newChild).timedGet(); + longList.put(newChildRef.getKey(), newChild); + + // Go back online and make sure we get the new item. + readRef.getDatabase().goOnline(); + TestHelpers.waitFor(readSemaphore); + TestHelpers.assertDeepEquals(longList, readSnapshots.get(1).getValue()); + } + + @Test + public void testNegativeIntegerKeys() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, + new MapBuilder().put("-1", "minus-one").put("0", "zero").put("1", "one").build()) + .timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(ref); + Map expected = new MapBuilder().put("-1", "minus-one").put("0", "zero") + .put("1", "one").build(); + Object result = snap.getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + @Ignore + public void testLocalServerValuesEventuallyButNotImmediatelyMatchServer() + throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final Semaphore completionSemaphore = new Semaphore(0); + final List readSnaps = new ArrayList<>(); + final List writeSnaps = new ArrayList<>(); + + reader.addValueEventListener(new ValueEventListener() { + @Override + public void onCancelled(DatabaseError error) { + } + + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + readSnaps.add(snapshot); + completionSemaphore.release(); + } + } + }); + + writer.addValueEventListener(new ValueEventListener() { + @Override + public void onCancelled(DatabaseError error) { + } + + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + if (snapshot.getValue() != null) { + writeSnaps.add(snapshot); + completionSemaphore.release(); + } + } + } + }); + + writer.setValue(ServerValue.TIMESTAMP, ServerValue.TIMESTAMP); + + TestHelpers.waitFor(completionSemaphore, 3); + + assertEquals(1, readSnaps.size()); + assertEquals(2, writeSnaps.size()); + assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(0).getValue()) < 3000); + assertTrue( + Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(0).getPriority()) < 3000); + assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(1).getValue()) < 3000); + assertTrue( + Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(1).getPriority()) < 3000); + assertFalse(writeSnaps.get(0).getValue().equals(writeSnaps.get(1).getValue())); + assertFalse(writeSnaps.get(0).getPriority().equals(writeSnaps.get(1).getPriority())); + assertEquals(writeSnaps.get(1).getValue(), readSnaps.get(0).getValue()); + assertEquals(writeSnaps.get(1).getPriority(), readSnaps.get(0).getPriority()); + } + + @Test + public void testBasicObjectMappingRoundTrip() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + + DumbBean bean = new DumbBean(); + bean.name = "bean"; + + DumbBean nestedBean = new DumbBean(); + nestedBean.name = "nested-bean"; + bean.nestedBean = nestedBean; + + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + new WriteFuture(ref, bean).timedGet(); + + final Semaphore done = new Semaphore(0); + ref.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + DumbBean bean = snapshot.getValue(DumbBean.class); + assertEquals("bean", bean.name); + assertEquals("nested-bean", bean.nestedBean.name); + assertNull(bean.nestedBean.nestedBean); + done.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testUpdateChildrenWithObjectMapping() throws InterruptedException { + + DumbBean bean1 = new DumbBean(); + bean1.name = "bean1"; + + DumbBean bean2 = new DumbBean(); + bean2.name = "bean2"; + + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore writeComplete = new Semaphore(0); + ref.updateChildren(new MapBuilder().put("bean1", bean1).put("bean2", bean2).build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + writeComplete.release(); + } + }); + TestHelpers.waitFor(writeComplete); + + final Semaphore done = new Semaphore(0); + ref.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Map beans = snapshot + .getValue(new GenericTypeIndicator>() { + }); + assertEquals("bean1", beans.get("bean1").name); + assertEquals("bean2", beans.get("bean2").name); + done.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testUpdateChildrenDeepUpdatesWithObjectMapping() throws InterruptedException { + + DumbBean bean1 = new DumbBean(); + bean1.name = "bean1"; + + DumbBean bean2 = new DumbBean(); + bean2.name = "bean2"; + + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore writeComplete = new Semaphore(0); + ref.updateChildren(new MapBuilder().put("bean1", bean1).put("deep/bean2", bean2).build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + writeComplete.release(); + } + }); + TestHelpers.waitFor(writeComplete); + + final Semaphore done = new Semaphore(0); + ref.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals("bean1", snapshot.child("bean1").getValue(DumbBean.class).name); + assertEquals("bean2", snapshot.child("deep/bean2").getValue(DumbBean.class).name); + done.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(done); + } + + private static class DumbBean { + public String name; + public DumbBean nestedBean; + } +} diff --git a/src/test/java/com/google/firebase/database/EventHelper.java b/src/test/java/com/google/firebase/database/integration/EventHelper.java similarity index 86% rename from src/test/java/com/google/firebase/database/EventHelper.java rename to src/test/java/com/google/firebase/database/integration/EventHelper.java index 3dfbfea59..e5a48e8cb 100644 --- a/src/test/java/com/google/firebase/database/EventHelper.java +++ b/src/test/java/com/google/firebase/database/integration/EventHelper.java @@ -1,6 +1,15 @@ -package com.google.firebase.database; - +package com.google.firebase.database.integration; + +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseException; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.ValueEventListener; import com.google.firebase.database.core.view.Event; +import com.google.firebase.testing.TestUtils; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -13,7 +22,6 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -/** User: greg Date: 5/28/13 Time: 9:06 AM */ class EventHelper { private List lookingFor; @@ -39,7 +47,7 @@ class EventHelper { initializationSemaphore = new Semaphore(0); } - public EventHelper addValueExpectation(DatabaseReference ref) { + EventHelper addValueExpectation(DatabaseReference ref) { if (!locations.contains(ref)) { toListen.add(ref); } @@ -47,7 +55,7 @@ public EventHelper addValueExpectation(DatabaseReference ref) { return this; } - public EventHelper addChildExpectation( + EventHelper addChildExpectation( DatabaseReference ref, Event.EventType eventType, String childName) throws DatabaseException { if (!locations.contains(ref)) { toListen.add(ref); @@ -56,11 +64,11 @@ public EventHelper addChildExpectation( return this; } - public EventHelper startListening() throws InterruptedException { + EventHelper startListening() throws InterruptedException { return startListening(false); } - public EventHelper startListening(boolean waitForInitialization) throws InterruptedException { + EventHelper startListening(boolean waitForInitialization) throws InterruptedException { waitingForInitialization = waitForInitialization; semaphore.acquire(1); locations.addAll(toListen); @@ -83,16 +91,16 @@ public int compare(DatabaseReference o1, DatabaseReference o2) { } }); - for (int i = 0; i < locationList.size(); ++i) { + for (DatabaseReference location : locationList) { if (waitForInitialization) { - uninitializedRefs.add(locationList.get(i)); + uninitializedRefs.add(location); } - listen(locationList.get(i)); + listen(location); } toListen.clear(); if (waitForInitialization) { initializationSemaphore.tryAcquire( - locationList.size(), TestConstants.TEST_TIMEOUT, TimeUnit.MILLISECONDS); + locationList.size(), TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); // Cut out the initialization events synchronized (this) { waitingForInitialization = false; @@ -175,7 +183,7 @@ private void recordEvent(EventRecord record) { } } - public void cleanup() { + void cleanup() { for (Map.Entry entry : valueListeners.entrySet()) { entry.getKey().removeEventListener(entry.getValue()); } @@ -207,9 +215,9 @@ private void checkSuccess() { } } - public boolean waitForEvents() throws InterruptedException { + boolean waitForEvents() throws InterruptedException { // Try waiting on the semaphore - if (!semaphore.tryAcquire(1, TestConstants.TEST_TIMEOUT, TimeUnit.MILLISECONDS)) { + if (!semaphore.tryAcquire(1, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { return false; } else { semaphore.release(1); diff --git a/src/test/java/com/google/firebase/database/integration/EventTestIT.java b/src/test/java/com/google/firebase/database/integration/EventTestIT.java new file mode 100644 index 000000000..b685de1a1 --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/EventTestIT.java @@ -0,0 +1,992 @@ +package com.google.firebase.database.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.core.ZombieVerifier; +import com.google.firebase.database.core.view.Event; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.testing.IntegrationTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EventTestIT { + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() throws TestFailure, TimeoutException, InterruptedException { + masterApp = IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + // NOTE: skipping test on valid types. + + @Test + public void testWriteLeafNodeExpectValue() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + final EventHelper readerHelper = new EventHelper().addValueExpectation(reader) + .startListening(true); + final EventHelper writerHelper = new EventHelper().addValueExpectation(writer) + .startListening(true); + + ZombieVerifier.verifyRepoZombies(refs); + + writer.setValue(42); + assertTrue(writerHelper.waitForEvents()); + assertTrue(readerHelper.waitForEvents()); + writerHelper.cleanup(); + readerHelper.cleanup(); + ZombieVerifier.verifyRepoZombies(refs); + } + + @Test + public void testWriteNestedLeafNodeWaitForEvents() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + EventHelper helper = + new EventHelper() + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "foo") + .addValueExpectation(ref) + .startListening(true); + + ZombieVerifier.verifyRepoZombies(ref); + + ref.child("foo").setValue(42); + assertTrue(helper.waitForEvents()); + ZombieVerifier.verifyRepoZombies(ref); + helper.cleanup(); + } + + @Test + public void testWriteTwoLeafNodeThenChangeThem() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + final EventHelper readHelper = + new EventHelper() + .addValueExpectation(reader.child("foo")) + .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "foo") + .addValueExpectation(reader) + .addValueExpectation(reader.child("bar")) + .addChildExpectation(reader, Event.EventType.CHILD_ADDED, "bar") + .addValueExpectation(reader) + .addValueExpectation(reader.child("foo")) + .addChildExpectation(reader, Event.EventType.CHILD_CHANGED, "foo") + .addValueExpectation(reader) + .startListening(true); + + final EventHelper writeHelper = + new EventHelper() + .addValueExpectation(writer.child("foo")) + .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "foo") + .addValueExpectation(writer) + .addValueExpectation(writer.child("bar")) + .addChildExpectation(writer, Event.EventType.CHILD_ADDED, "bar") + .addValueExpectation(writer) + .addValueExpectation(writer.child("foo")) + .addChildExpectation(writer, Event.EventType.CHILD_CHANGED, "foo") + .addValueExpectation(writer) + .startListening(true); + + ZombieVerifier.verifyRepoZombies(refs); + + writer.child("foo").setValue(42); + writer.child("bar").setValue(24); + + writer.child("foo").setValue(31415); + assertTrue(writeHelper.waitForEvents()); + assertTrue(readHelper.waitForEvents()); + ZombieVerifier.verifyRepoZombies(refs); + + readHelper.cleanup(); + writeHelper.cleanup(); + } + + @Test + public void testSetMultipleEventListenersOnSameNode() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + final EventHelper writeHelper = new EventHelper().addValueExpectation(writer) + .startListening(true); + final EventHelper writeHelper2 = new EventHelper().addValueExpectation(writer) + .startListening(true); + final EventHelper readHelper = new EventHelper().addValueExpectation(reader) + .startListening(true); + final EventHelper readHelper2 = new EventHelper().addValueExpectation(reader) + .startListening(true); + + ZombieVerifier.verifyRepoZombies(refs); + + writer.setValue(42); + assertTrue(writeHelper.waitForEvents()); + assertTrue(writeHelper2.waitForEvents()); + assertTrue(readHelper.waitForEvents()); + assertTrue(readHelper2.waitForEvents()); + ZombieVerifier.verifyRepoZombies(refs); + + writeHelper.cleanup(); + writeHelper2.cleanup(); + readHelper.cleanup(); + readHelper2.cleanup(); + } + + @Test + public void testSetDataMultipleTimesEnsureValueIsCalledAppropriately() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + ReadFuture readFuture = ReadFuture.untilEquals(ref, 2L, /*ignoreFirstNull=*/ true); + ZombieVerifier.verifyRepoZombies(ref); + + for (int i = 0; i < 3; ++i) { + ref.setValue(i); + } + + List events = readFuture.timedGet(); + for (long i = 0; i < 3; ++i) { + DataSnapshot snap = events.get((int) i).getSnapshot(); + assertEquals(i, snap.getValue()); + } + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testUnsubscribeEventsAndConfirmEventsNoLongerFire() + throws TestFailure, ExecutionException, TimeoutException, + InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final AtomicInteger callbackCount = new AtomicInteger(0); + + final ValueEventListener listener = + ref.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + callbackCount.incrementAndGet(); + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + ZombieVerifier.verifyRepoZombies(ref); + + for (int i = 0; i < 3; ++i) { + ref.setValue(i); + } + + TestHelpers.waitForRoundtrip(ref); + ref.removeEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + + for (int i = 10; i < 13; ++i) { + ref.setValue(i); + } + + for (int i = 20; i < 22; ++i) { + ref.setValue(i); + } + new WriteFuture(ref, 22).timedGet(); + assertEquals(3, callbackCount.get()); + } + + @Test + public void testSubscribeThenUnsubscribeWithoutProblems() + throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + ValueEventListener listener = + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) {} + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }; + + ValueEventListener listenerHandle = ref.addValueEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + ref.removeEventListener(listenerHandle); + ZombieVerifier.verifyRepoZombies(ref); + ValueEventListener listenerHandle2 = ref.addValueEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + ref.removeEventListener(listenerHandle2); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testSubscribeThenUnsubscribeWithoutProblemsWithLimit() + throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + ValueEventListener listener = + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) {} + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }; + + ValueEventListener listenerHandle = ref.limitToLast(100).addValueEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + ref.removeEventListener(listenerHandle); + ZombieVerifier.verifyRepoZombies(ref); + ValueEventListener listenerHandle2 = ref.limitToLast(100).addValueEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + ref.removeEventListener(listenerHandle2); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testWriteJsonAndGetGranularEvents() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + final AtomicBoolean readerSawA = new AtomicBoolean(false); + final AtomicBoolean readerSawB = new AtomicBoolean(false); + final AtomicBoolean readerSawB2 = new AtomicBoolean(false); + final AtomicBoolean writerSawA = new AtomicBoolean(false); + final AtomicBoolean writerSawB = new AtomicBoolean(false); + final AtomicBoolean writerSawB2 = new AtomicBoolean(false); + final Semaphore readerReady = new Semaphore(0); + final Semaphore writerReady = new Semaphore(0); + + reader + .child("a") + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Long val = (Long) snapshot.getValue(); + if (val != null && val == 10L) { + assertTrue(readerSawA.compareAndSet(false, true)); + readerReady.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + reader + .child("b") + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Long val = (Long) snapshot.getValue(); + if (val != null) { + if (val == 20L) { + assertTrue(readerSawB.compareAndSet(false, true)); + } else if (val == 30L) { + assertTrue(readerSawB2.compareAndSet(false, true)); + } + readerReady.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + writer + .child("a") + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Long val = (Long) snapshot.getValue(); + if (val != null) { + assertTrue(writerSawA.compareAndSet(false, true)); + writerReady.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + writer + .child("b") + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Long val = (Long) snapshot.getValue(); + if (val != null) { + if (val == 20L) { + assertTrue(writerSawB.compareAndSet(false, true)); + } else if (val == 30L) { + assertTrue(writerSawB2.compareAndSet(false, true)); + } + writerReady.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ZombieVerifier.verifyRepoZombies(refs); + + writer.setValue(MapBuilder.of("a", 10, "b", 20)); + TestHelpers.waitFor(writerReady, 2); + TestHelpers.waitFor(readerReady, 2); + + writer.setValue(MapBuilder.of("a", 10, "b", 30)); + TestHelpers.waitFor(writerReady); + TestHelpers.waitFor(readerReady); + } + + @Test + public void testValueIsTriggeredForEmptyNodes() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + DataSnapshot snap = new ReadFuture(ref).timedGet().get(0).getSnapshot(); + ZombieVerifier.verifyRepoZombies(ref); + assertNull(snap.getValue()); + } + + @Test + public void testLeafNodeTurnsIntoAnInternalNode() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 4); + + ZombieVerifier.verifyRepoZombies(ref); + + ref.setValue(42); + ref.setValue(MapBuilder.of("a", 2)); + ref.setValue(84); + ref.setValue(null); + + List events = readFuture.timedGet(); + ZombieVerifier.verifyRepoZombies(ref); + + assertEquals(42L, events.get(0).getSnapshot().getValue()); + assertEquals(2L, events.get(1).getSnapshot().child("a").getValue()); + assertEquals(84L, events.get(2).getSnapshot().getValue()); + assertNull(events.get(3).getSnapshot().getValue()); + } + + @Test + public void testRegisterTheSameCallbackMultipleTimes() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final AtomicInteger callbackCount = new AtomicInteger(0); + ValueEventListener listener = + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + callbackCount.incrementAndGet(); + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }; + + ref.addValueEventListener(listener); + ref.addValueEventListener(listener); + ref.addValueEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + + new WriteFuture(ref, 42).timedGet(); + assertEquals(3, callbackCount.get()); + + ref.removeEventListener(listener); + new WriteFuture(ref, 84).timedGet(); + assertEquals(5, callbackCount.get()); + ZombieVerifier.verifyRepoZombies(ref); + + ref.removeEventListener(listener); + new WriteFuture(ref, 168).timedGet(); + assertEquals(6, callbackCount.get()); + ZombieVerifier.verifyRepoZombies(ref); + + ref.removeEventListener(listener); + new WriteFuture(ref, 376).timedGet(); + assertEquals(6, callbackCount.get()); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testUnregisterSameCallbackTooManyTimes() + throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + ValueEventListener listener = + ref.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + // no-op + } + }); + + ZombieVerifier.verifyRepoZombies(ref); + + ref.removeEventListener(listener); + ref.removeEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testRemovesHappenImmediately() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + final Semaphore blockSem = new Semaphore(0); + final Semaphore endingSemaphore = new Semaphore(0); + + final AtomicBoolean called = new AtomicBoolean(false); + ref.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + assertTrue(called.compareAndSet(false, true)); + try { + TestHelpers.waitFor(blockSem); + } catch (InterruptedException e) { + e.printStackTrace(); + } + ref.removeEventListener(this); + try { + // this doesn't block immediately because we are already on the repo + // thread. + // we kick off the verify and let the unit test block on the + // endingsemaphore + ZombieVerifier.verifyRepoZombies(ref, endingSemaphore); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + ZombieVerifier.verifyRepoZombies(ref); + + ref.setValue(42); + TestHelpers.waitForQueue(ref); + ref.setValue(84); + blockSem.release(); + new WriteFuture(ref, null).timedGet(); + TestHelpers.waitFor(endingSemaphore); + } + + @Test + public void testRemovesHappenImmediatelyOnOuterRef() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + final Semaphore gotInitialEvent = new Semaphore(0); + final Semaphore blockSem = new Semaphore(0); + final Semaphore endingSemaphore = new Semaphore(0); + + final AtomicBoolean called = new AtomicBoolean(false); + ref.limitToFirst(5) + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + gotInitialEvent.release(); + if (snapshot.getValue() != null) { + assertTrue(called.compareAndSet(false, true)); + try { + TestHelpers.waitFor(blockSem); + } catch (InterruptedException e) { + e.printStackTrace(); + } + ref.removeEventListener(this); + try { + // this doesn't block immediately because we are already on the + // repo thread. + // we kick off the verify and let the unit test block on the + // endingsemaphore + ZombieVerifier.verifyRepoZombies(ref, endingSemaphore); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + ZombieVerifier.verifyRepoZombies(ref); + + TestHelpers.waitFor(gotInitialEvent); + + ref.child("a").setValue(42); + TestHelpers.waitForQueue(ref); + ref.child("b").setValue(84); + blockSem.release(); + new WriteFuture(ref, null).timedGet(); + TestHelpers.waitFor(endingSemaphore); + } + + @Test + public void testRemovesHappenImmediatelyOnMultipleRef() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + final Semaphore gotInitialEvent = new Semaphore(0); + final Semaphore blockSem = new Semaphore(0); + final Semaphore endingSemaphore = new Semaphore(0); + + final AtomicBoolean called = new AtomicBoolean(false); + ValueEventListener listener = + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + gotInitialEvent.release(); + if (snapshot.getValue() != null) { + assertTrue(called.compareAndSet(false, true)); + try { + TestHelpers.waitFor(blockSem); + } catch (InterruptedException e) { + e.printStackTrace(); + } + ref.removeEventListener(this); + try { + // this doesn't block immediately because we are already on the repo + // thread. + // we kick off the verify and let the unit test block on the + // endingsemaphore + ZombieVerifier.verifyRepoZombies(ref, endingSemaphore); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }; + + ref.addValueEventListener(listener); + ref.limitToFirst(5).addValueEventListener(listener); + ZombieVerifier.verifyRepoZombies(ref); + + TestHelpers.waitFor(gotInitialEvent, 2); + ref.child("a").setValue(42); + TestHelpers.waitForQueue(ref); + ref.child("b").setValue(84); + blockSem.release(); + new WriteFuture(ref, null).timedGet(); + TestHelpers.waitFor(endingSemaphore); + } + + @Test + public void testRemovesHappenImmediatelyChild() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + final Semaphore blockSem = new Semaphore(0); + final Semaphore endingSemaphore = new Semaphore(0); + + final AtomicBoolean called = new AtomicBoolean(false); + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + if (snapshot.getValue() != null) { + assertTrue(called.compareAndSet(false, true)); + try { + TestHelpers.waitFor(blockSem); + } catch (InterruptedException e) { + e.printStackTrace(); + } + ref.removeEventListener(this); + try { + // This doesn't block immediately because we are already on the repo + // thread. We kick off the verify and let the unit test block on the + // ending semaphore. + ZombieVerifier.verifyRepoZombies(ref, endingSemaphore); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) {} + + @Override + public void onChildRemoved(DataSnapshot snapshot) {} + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) {} + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ZombieVerifier.verifyRepoZombies(ref); + + ref.child("a").setValue(42); + TestHelpers.waitForQueue(ref); + ref.child("b").setValue(84); + blockSem.release(); + new WriteFuture(ref, null).timedGet(); + TestHelpers.waitFor(endingSemaphore); + } + + @Test + public void testOnceFiresExactlyOnce() + throws TestFailure, ExecutionException, TimeoutException, + InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final AtomicInteger called = new AtomicInteger(0); + ref.addListenerForSingleValueEvent( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + called.incrementAndGet(); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + ZombieVerifier.verifyRepoZombies(ref); + + ref.setValue(42); + ref.setValue(84); + new WriteFuture(ref, null).timedGet(); + assertEquals(1, called.get()); + ZombieVerifier.verifyRepoZombies(ref); + } + + // NOTE: skipped tests on testing 'once' with child events. Not supported in Java SDK + + @Test + public void testValueOnEmptyChildFires() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + DataSnapshot snap = new ReadFuture(ref.child("test")).timedGet().get(0).getSnapshot(); + assertNull(snap.getValue()); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testValueOnEmptyChildFiresImmediately() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + // Sync parent + new ReadFuture(ref).timedGet(); + + DataSnapshot snap = new ReadFuture(ref.child("test")).timedGet().get(0).getSnapshot(); + assertNull(snap.getValue()); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testChildEventsAreRaised() + throws TestFailure, ExecutionException, TimeoutException, + InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final Map firstValue = + new MapBuilder() + .put("a", MapBuilder.of(".value", "x", ".priority", 0)) + .put("b", MapBuilder.of(".value", "x", ".priority", 1)) + .put("c", MapBuilder.of(".value", "x", ".priority", 2)) + .put("d", MapBuilder.of(".value", "x", ".priority", 3)) + .put("e", MapBuilder.of(".value", "x", ".priority", 4)) + .put("f", MapBuilder.of(".value", "x", ".priority", 5)) + .put("g", MapBuilder.of(".value", "x", ".priority", 6)) + .put("h", MapBuilder.of(".value", "x", ".priority", 7)) + .build(); + + final Map secondValue = + new MapBuilder() + // added + .put("aa", MapBuilder.of(".value", "x", ".priority", 0)) + .put("b", MapBuilder.of(".value", "x", ".priority", 1)) + // added + .put("bb", MapBuilder.of(".value", "x", ".priority", 2)) + // removed c + // changed + .put("d", MapBuilder.of(".value", "y", ".priority", 3)) + .put("e", MapBuilder.of(".value", "x", ".priority", 4)) + // moved + .put("a", MapBuilder.of(".value", "x", ".priority", 6)) + // moved + .put("f", MapBuilder.of(".value", "x", ".priority", 7)) + // removed g + // changed + .put("h", MapBuilder.of(".value", "y", ".priority", 7)) + .build(); + + final List events = new ArrayList<>(); + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + events.add("added " + snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + events.add("changed " + snapshot.getKey()); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + events.add("removed " + snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + events.add("moved " + snapshot.getKey()); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + new WriteFuture(ref, firstValue).timedGet(); + events.clear(); + new WriteFuture(ref, secondValue).timedGet(); + + List expected = + Arrays.asList( + "removed c", + "removed g", + "added aa", + "added bb", + "moved a", + "moved f", + "changed d", + "changed a", + "changed f", + "changed h"); + String expectedString = expected.toString(); + String actualString = events.toString(); + assertEquals(expectedString, actualString); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testChildEventsAreRaisedWithAQuery() + throws TestFailure, ExecutionException, TimeoutException, + InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final Map firstValue = + new MapBuilder() + .put("a", MapBuilder.of(".value", "x", ".priority", 0)) + .put("b", MapBuilder.of(".value", "x", ".priority", 1)) + .put("c", MapBuilder.of(".value", "x", ".priority", 2)) + .put("d", MapBuilder.of(".value", "x", ".priority", 3)) + .put("e", MapBuilder.of(".value", "x", ".priority", 4)) + .put("f", MapBuilder.of(".value", "x", ".priority", 5)) + .put("g", MapBuilder.of(".value", "x", ".priority", 6)) + .put("h", MapBuilder.of(".value", "x", ".priority", 7)) + .build(); + + final Map secondValue = + new MapBuilder() + // added + .put("aa", MapBuilder.of(".value", "x", ".priority", 0)) + .put("b", MapBuilder.of(".value", "x", ".priority", 1)) + // added + .put("bb", MapBuilder.of(".value", "x", ".priority", 2)) + // removed c + // changed + .put("d", MapBuilder.of(".value", "y", ".priority", 3)) + .put("e", MapBuilder.of(".value", "x", ".priority", 4)) + // moved + .put("a", MapBuilder.of(".value", "x", ".priority", 6)) + // moved + .put("f", MapBuilder.of(".value", "x", ".priority", 7)) + // removed g + // changed + .put("h", MapBuilder.of(".value", "y", ".priority", 7)) + .build(); + + final List events = new ArrayList<>(); + ref.limitToLast(10) + .addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + events.add("added " + snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + events.add("changed " + snapshot.getKey()); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + events.add("removed " + snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + events.add("moved " + snapshot.getKey()); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + new WriteFuture(ref, firstValue).timedGet(); + events.clear(); + new WriteFuture(ref, secondValue).timedGet(); + + List expected = + Arrays.asList( + "removed c", + "removed g", + "added aa", + "added bb", + "moved a", + "moved f", + "changed d", + "changed a", + "changed f", + "changed h"); + String expectedString = expected.toString(); + String actualString = events.toString(); + assertEquals(expectedString, actualString); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testPriorityChange() + throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final EventHelper helper = + new EventHelper() + .addValueExpectation(ref.child("bar")) + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "bar") + .addValueExpectation(ref) + .addValueExpectation(ref.child("foo")) + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "foo") + .addValueExpectation(ref) + .startListening(true); + + ref.child("bar").setValue(42, 10); + TestHelpers.waitForRoundtrip(ref); + ref.child("foo").setValue(42, 20); + + assertTrue(helper.waitForEvents()); + helper + .addValueExpectation(ref.child("bar")) + .addChildExpectation(ref, Event.EventType.CHILD_MOVED, "bar") + .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "bar") + .addValueExpectation(ref) + .startListening(); + + ref.child("bar").setPriority(30); + assertTrue(helper.waitForEvents()); + helper.cleanup(); + ZombieVerifier.verifyRepoZombies(ref); + } + + @Test + public void testPriorityChange2() + throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + EventHelper helper = + new EventHelper() + .addValueExpectation(ref.child("bar")) + .addValueExpectation(ref.child("foo")) + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "bar") + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "foo") + .addValueExpectation(ref) + .startListening(true); + + ZombieVerifier.verifyRepoZombies(ref); + ref.setValue( + MapBuilder.of( + "bar", MapBuilder.of(".value", 42, ".priority", 10), + "foo", MapBuilder.of(".value", 42, ".priority", 20))); + assertTrue(helper.waitForEvents()); + helper + .addValueExpectation(ref.child("bar")) + .addChildExpectation(ref, Event.EventType.CHILD_MOVED, "bar") + .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "bar") + .addValueExpectation(ref) + .startListening(); + + ZombieVerifier.verifyRepoZombies(ref); + ref.setValue( + MapBuilder.of( + "foo", MapBuilder.of(".value", 42, ".priority", 20), + "bar", MapBuilder.of(".value", 42, ".priority", 30))); + assertTrue(helper.waitForEvents()); + helper.cleanup(); + ZombieVerifier.verifyRepoZombies(ref); + } +} diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java index e73968aa9..dd5099ffc 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java @@ -19,11 +19,13 @@ import com.google.firebase.testing.IntegrationTestUtils.AppHttpClient; import com.google.firebase.testing.IntegrationTestUtils.ResponseInfo; import com.google.firebase.testing.ServiceAccount; +import com.google.firebase.testing.TestUtils; import java.io.IOException; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -142,13 +144,15 @@ public void onComplete(Task task) { lock.countDown(); } }); - boolean finished = lock.await(IntegrationTestUtils.ASYNC_WAIT_TIME_MS, TimeUnit.MILLISECONDS); + boolean finished = lock.await(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); if (shouldTimeout) { - assertTrue("Write finished.", !finished); + assertTrue("Write finished (expected to timeout).", !finished); } else if (shouldSucceed) { - assertTrue("Write failed.", finished && success.get()); + assertTrue("Write timed out (expected to succeed)", finished); + assertTrue("Write failed (expected to succeed).", success.get()); } else { - assertTrue("Write successful.", finished && !success.get()); + assertTrue("Write timed out (expected to fail).", finished); + assertTrue("Write successful (expected to fail).", !success.get()); } } @@ -171,13 +175,15 @@ public void onCancelled(DatabaseError databaseError) { } }); - boolean finished = lock.await(IntegrationTestUtils.ASYNC_WAIT_TIME_MS, TimeUnit.MILLISECONDS); + boolean finished = lock.await(TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); if (shouldTimeout) { - assertTrue("Read finished.", !finished); + assertTrue("Read finished (expected to timeout).", !finished); } else if (shouldSucceed) { - assertTrue("Read failed.", finished && success.get()); + assertTrue("Read timed out (expected to succeed).", finished); + assertTrue("Read failed (expected to succeed).", success.get()); } else { - assertTrue("Read successful.", finished && !success.get()); + assertTrue("Read timed out (expected to fail).", finished); + assertTrue("Read successful (expected to fail).", !success.get()); } } diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java index ae2f79d9b..b12947b7e 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java @@ -40,22 +40,24 @@ public void testGetInstanceForApp() { FirebaseDatabase db = FirebaseDatabase.getInstance(app); assertEquals(app.getOptions().getDatabaseUrl(), db.getReference().toString()); } - + + @Test public void testNullDatabaseUrl() { FirebaseApp app = appWithDbUrl(null, "nullDbUrl"); try { FirebaseDatabase.getInstance(app); fail("no error thrown for getInstance() with null URL"); - } catch (DatabaseException expected) { + } catch (DatabaseException expected) { // ignore } } - + + @Test public void testMalformedDatabaseUrlInOptions() { FirebaseApp app = appWithDbUrl("not-a-url", "malformedDbUrlInOptions"); try { FirebaseDatabase.getInstance(app); fail("no error thrown for getInstance() with malformed URL"); - } catch (DatabaseException expected) { + } catch (DatabaseException expected) { // ignore } } @@ -65,7 +67,7 @@ public void testMalformedDatabaseUrlInGetInstance() { try { FirebaseDatabase.getInstance(app, "not-a-url"); fail("no error thrown for getInstance() with malformed URL"); - } catch (DatabaseException expected) { + } catch (DatabaseException expected) { // ignore } } @@ -76,7 +78,7 @@ public void testDatabaseUrlWithPathInOptions() { try { FirebaseDatabase.getInstance(app); fail("no error thrown for DB URL with path"); - } catch (DatabaseException expected) { + } catch (DatabaseException expected) { // ignore } } @@ -87,7 +89,7 @@ public void testDatabaseUrlWithPathInGetInstance() { FirebaseDatabase.getInstance(app, IntegrationTestUtils.getDatabaseUrl() + "/paths/are/not/allowed"); fail("no error thrown for DB URL with path"); - } catch (DatabaseException expected) { + } catch (DatabaseException expected) { // ignore } } @@ -117,7 +119,7 @@ public void testGetReferenceFromURLWithPath() { @Test(expected = DatabaseException.class) public void testGetReferenceThrowsWithBadUrl() { FirebaseDatabase db = FirebaseDatabase.getInstance(); - db.getReferenceFromUrl("http://tests2.fblocal.com:9000"); + db.getReferenceFromUrl("http://tests2.fake-firebaseio.com:9000"); } private static FirebaseApp appWithDbUrl(String dbUrl, String name) { diff --git a/src/test/java/com/google/firebase/database/integration/InfoTestIT.java b/src/test/java/com/google/firebase/database/integration/InfoTestIT.java new file mode 100644 index 000000000..47c5a7025 --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/InfoTestIT.java @@ -0,0 +1,197 @@ +package com.google.firebase.database.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseException; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.MutableData; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.Transaction; +import com.google.firebase.database.core.DatabaseConfig; +import com.google.firebase.database.core.RepoManager; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.testing.IntegrationTestUtils; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class InfoTestIT { + + @BeforeClass + public static void setUpClass() throws IOException { + IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetReferenceToInfoNodes() { + DatabaseReference root = FirebaseDatabase.getInstance().getReference(); + String dbUrl = FirebaseApp.getInstance().getOptions().getDatabaseUrl(); + assertEquals(dbUrl + "/.info", root.child(".info").toString()); + assertEquals(dbUrl + "/.info/foo", root.child(".info/foo").toString()); + } + + @Test + public void testInfoNodeSetValue() { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(".info"); + try { + ref.setValue("hi"); + fail("Should not be allowed"); + } catch (DatabaseException expected) { + // No-op, expected + } + } + + @Test + public void testInfoNodeSetValueWithPriority() { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(".info"); + try { + ref.setValue("hi", 5); + fail("Should not be allowed"); + } catch (DatabaseException expected) { + // No-op, expected + } + } + + @Test + public void testInfoNodeSetPriority() { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(".info"); + try { + ref.setPriority("hi"); + fail("Should not be allowed"); + } catch (DatabaseException expected) { + // No-op, expected + } + } + + @Test + public void testInfoNodeRunTransaction() { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(".info"); + try { + ref.runTransaction( + new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + fail("Should not get called"); + return null; + } + + @Override + public void onComplete( + DatabaseError error, boolean committed, DataSnapshot currentData) { + fail("Should not get called"); + } + }); + fail("Should not be allowed"); + } catch (DatabaseException expected) { + // No-op, expected + } + } + + @Test + public void testInfoNodeRemoveValue() { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(".info"); + try { + ref.removeValue(); + fail("Should not be allowed"); + } catch (DatabaseException expected) { + // No-op, expected + } + } + + @Test + public void testInfoNodeChildSetValue() { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(".info"); + try { + ref.child("test").setValue("hi"); + fail("Should not be allowed"); + } catch (DatabaseException expected) { + // No-op, expected + } + } + + @Test + public void testInfoConnected() throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); + DataSnapshot snap = new ReadFuture(ref.child(".info/connected")) + .timedGet().get(0).getSnapshot(); + assertTrue(snap.getValue() instanceof Boolean); + } + + @Test + public void testManualConnectionManagement() + throws TestFailure, TimeoutException, InterruptedException { + FirebaseApp app = IntegrationTestUtils.initApp("testManualConnectionManagement"); + DatabaseReference ref = FirebaseDatabase.getInstance(app).getReference(); + + // Wait until we're connected to database + ReadFuture.untilEquals(ref.child(".info/connected"), true).timedGet(); + + ref.getDatabase().goOffline(); + + try { + // Ensure we're disconnected + DataSnapshot snap = + new ReadFuture(ref.child(".info/connected")).timedGet().get(0).getSnapshot(); + assertFalse((Boolean) snap.getValue()); + + // Ensure that we don't automatically reconnect upon new DatabaseReference creation + DatabaseReference refDup = FirebaseDatabase.getInstance(app).getReference(); + try { + ReadFuture.untilEquals(refDup.child(".info/connected"), true) + .timedGet(1500, TimeUnit.MILLISECONDS); + fail("Did not timeout"); // We should never get here! + } catch (TimeoutException e) { // + } + } finally { + ref.getDatabase().goOnline(); + // Ensure we're reconnected + ReadFuture.untilEquals(ref.child(".info/connected"), true).timedGet(); + } + } + + @Test + public void testInfoConnectedOnDisconnect() + throws TestFailure, TimeoutException, InterruptedException { + FirebaseApp app = IntegrationTestUtils.initApp("testInfoConnectedOnDisconnect"); + DatabaseReference ref = FirebaseDatabase.getInstance(app).getReference(); + + // Wait until we're connected + ReadFuture.untilEquals(ref.child(".info/connected"), true).timedGet(); + + DatabaseConfig ctx = TestHelpers.getDatabaseConfig(app); + RepoManager.interrupt(ctx); + try { + DataSnapshot snap = + new ReadFuture(ref.child(".info/connected")).timedGet().get(0).getSnapshot(); + assertFalse((Boolean) snap.getValue()); + } finally { + RepoManager.resume(ctx); + } + } + + @Test + public void testInfoServerTimeOffset() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); + DataSnapshot snap = + new ReadFuture(ref.child(".info/serverTimeOffset")).timedGet().get(0).getSnapshot(); + assertTrue(snap.getValue() instanceof Long); + } +} diff --git a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java new file mode 100644 index 000000000..dc68df17d --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java @@ -0,0 +1,983 @@ +package com.google.firebase.database.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.DatabaseReference.CompletionListener; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.Query; +import com.google.firebase.database.TestChildEventListener; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.testing.IntegrationTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class OrderByTestIT { + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() { + masterApp = IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() throws IOException { + uploadRules(masterApp, "{\"rules\": {\".read\": true, \".write\": true}}"); + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + private static String formatRules(DatabaseReference ref, String rules) { + return String.format( + "{\"rules\": {\".read\": true, \".write\": true, \"%s\": %s}}", ref.getKey(), rules); + } + + private static void uploadRules(FirebaseApp app, String rules) throws IOException { + IntegrationTestUtils.AppHttpClient client = new IntegrationTestUtils.AppHttpClient(app); + IntegrationTestUtils.ResponseInfo response = client.put("/.settings/rules.json", rules); + assertEquals(200, response.getStatus()); + } + + @Test + public void testBasicIndexing() throws IOException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + String rules = formatRules(ref, "{ \".indexOn\": \"nuggets\"}"); + + uploadRules(masterApp, rules); + + final Semaphore semaphore = new Semaphore(0); + + CompletionListener listener = + new CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + Assert.assertNull(error); + semaphore.release(); + } + }; + ref.push() + .setValue(TestHelpers.fromJsonString("{\"name\": \"Andrew\", \"nuggets\": 35}"), listener); + ref.push() + .setValue(TestHelpers.fromJsonString("{\"name\": \"Rob\", \"nuggets\": 40}"), listener); + ref.push() + .setValue(TestHelpers.fromJsonString("{\"name\": \"Greg\", \"nuggets\": 38}"), listener); + + TestHelpers.waitFor(semaphore, 3); + + final List names = new ArrayList<>(); + + final ChildEventListener testListener = + ref.orderByChild("nuggets") + .addChildEventListener( + new TestChildEventListener() { + @SuppressWarnings("unchecked") + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + names.add(((Map) snapshot.getValue()).get("name")); + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore, 3); + Assert.assertEquals(ImmutableList.of("Andrew", "Greg", "Rob"), names); + TestHelpers.waitForRoundtrip(ref); + ref.removeEventListener(testListener); + } + + @Test + public void testUseFallbackThenDefineIndex() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + + DatabaseReference writer = IntegrationTestUtils.getRandomNode(masterApp) ; + DatabaseReference readerReference = FirebaseDatabase.getInstance(masterApp).getReference(); + DatabaseReference reader = readerReference.child(writer.getPath().toString()); + + Map foo1 = + TestHelpers.fromJsonString( + "{ " + + "\"a\": {\"order\": 2, \"foo\": 1}, " + + "\"b\": {\"order\": 0}, " + + "\"c\": {\"order\": 1, \"foo\": false}, " + + "\"d\": {\"order\": 3, \"foo\": \"hello\"} }"); + + new WriteFuture(writer, foo1).timedGet(); + + final List snapshots = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + Query query = reader.orderByChild("order").limitToLast(2); + final ValueEventListener listener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + snapshots.add(snapshot); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + TestHelpers.waitFor(semaphore); + + Assert.assertEquals(1, snapshots.size()); + Map expected = MapBuilder.of( + "d", MapBuilder.of("order", 3L, "foo", "hello"), + "a", MapBuilder.of("order", 2L, "foo", 1L)); + Assert.assertEquals(expected, snapshots.get(0).getValue()); + + uploadRules(masterApp, formatRules(reader, "{ \".indexOn\": \"order\" }")); + + Map fooE = TestHelpers.fromJsonString("{\"order\": 1.5, \"foo\": true}"); + new WriteFuture(writer.child("e"), fooE).timedGet(); + TestHelpers.waitForRoundtrip(reader); + + Map fooF = + TestHelpers.fromJsonString("{\"order\": 4, \"foo\": {\"bar\": \"baz\"}}"); + new WriteFuture(writer.child("f"), fooF).timedGet(); + TestHelpers.waitForRoundtrip(reader); + + TestHelpers.waitFor(semaphore); + Assert.assertEquals(2, snapshots.size()); + + Map expected2 = + new MapBuilder() + .put( + "f", + new MapBuilder() + .put("order", 4L) + .put("foo", MapBuilder.of("bar", "baz")) + .build()) + .put("d", MapBuilder.of("order", 3L, "foo", "hello")) + .build(); + Assert.assertEquals(expected2, snapshots.get(1).getValue()); + + // cleanup + TestHelpers.waitForRoundtrip(reader); + reader.removeEventListener(listener); + } + + @Test + public void testSnapshotIterationOrder() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + Map initial = + TestHelpers.fromJsonString( + "{" + + "\"alex\": {\"nuggets\": 60}," + + "\"greg\": {\"nuggets\": 52}," + + "\"rob\": {\"nuggets\": 56}," + + "\"vassili\": {\"nuggets\": 55.5}," + + "\"tony\": {\"nuggets\": 52}" + + "}"); + + Query query = ref.orderByChild("nuggets"); + + final List valueOrder = new ArrayList<>(); + final List childOrder = new ArrayList<>(); + final List childPrevNames = new ArrayList<>(); + final ValueEventListener valueListener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + for (DataSnapshot child : snapshot.getChildren()) { + valueOrder.add(child.getKey()); + } + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + final ChildEventListener testListener = + query.addChildEventListener( + new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + childOrder.add(snapshot.getKey()); + childPrevNames.add(previousChildName); + } + }); + + new WriteFuture(ref, initial).timedGet(); + + List expectedOrder = Arrays.asList("greg", "tony", "vassili", "rob", "alex"); + List expectedPrevNames = Arrays.asList(null, "greg", "tony", "vassili", "rob"); + Assert.assertEquals(expectedOrder, valueOrder); + Assert.assertEquals(expectedOrder, childOrder); + Assert.assertEquals(expectedPrevNames, childPrevNames); + + // cleanup + TestHelpers.waitForRoundtrip(ref); + ref.removeEventListener(testListener); + ref.removeEventListener(valueListener); + } + + @Test + public void testDeepPathsForIndex() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + Map initial = + TestHelpers.fromJsonString( + "{" + + "\"alex\": {\"deep\": {\"nuggets\": 60}}," + + "\"greg\": {\"deep\": {\"nuggets\": 52}}," + + "\"rob\": {\"deep\": {\"nuggets\": 56}}," + + "\"vassili\": {\"deep\": {\"nuggets\": 55.5}}," + + "\"tony\": {\"deep\": {\"nuggets\": 52}}" + + "}"); + + new WriteFuture(ref, initial).timedGet(); + + Query query = ref.orderByChild("deep/nuggets").limitToFirst(3); + + final List valueOrder = new ArrayList<>(); + final List childOrder = new ArrayList<>(); + final List childPrevNames = new ArrayList<>(); + final ValueEventListener valueListener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + for (DataSnapshot child : snapshot.getChildren()) { + valueOrder.add(child.getKey()); + } + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + final ChildEventListener testListener = + query.addChildEventListener( + new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + childOrder.add(snapshot.getKey()); + childPrevNames.add(previousChildName); + } + }); + + TestHelpers.waitForRoundtrip(ref); + + List expectedOrder = Arrays.asList("greg", "tony", "vassili"); + List expectedPrevNames = Arrays.asList(null, "greg", "tony"); + Assert.assertEquals(expectedOrder, valueOrder); + Assert.assertEquals(expectedOrder, childOrder); + Assert.assertEquals(expectedPrevNames, childPrevNames); + + // cleanup + ref.removeEventListener(testListener); + ref.removeEventListener(valueListener); + } + + @Test + public void testSnapshotIterationOrderForValueIndex() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + Map initial = + TestHelpers.fromJsonString( + "{" + + "\"alex\": 60," + + "\"greg\": 52," + + "\"rob\": 56," + + "\"vassili\": 55.5," + + "\"tony\": 52" + + "}"); + + Query query = ref.orderByValue(); + + final List valueOrder = new ArrayList<>(); + final List childOrder = new ArrayList<>(); + final List childPrevNames = new ArrayList<>(); + final ValueEventListener valueListener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + for (DataSnapshot child : snapshot.getChildren()) { + valueOrder.add(child.getKey()); + } + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + final ChildEventListener testListener = + query.addChildEventListener( + new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + childOrder.add(snapshot.getKey()); + childPrevNames.add(previousChildName); + } + }); + + new WriteFuture(ref, initial).timedGet(); + + List expectedOrder = Arrays.asList("greg", "tony", "vassili", "rob", "alex"); + List expectedPrevNames = Arrays.asList(null, "greg", "tony", "vassili", "rob"); + Assert.assertEquals(expectedOrder, valueOrder); + Assert.assertEquals(expectedOrder, childOrder); + Assert.assertEquals(expectedPrevNames, childPrevNames); + + // cleanup + TestHelpers.waitForRoundtrip(ref); + ref.removeEventListener(testListener); + ref.removeEventListener(valueListener); + } + + @Test + public void testCildMovedEvents() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + Map initial = + TestHelpers.fromJsonString( + "{" + + "\"alex\": {\"nuggets\": 60}," + + "\"greg\": {\"nuggets\": 52}," + + "\"rob\": {\"nuggets\": 56}," + + "\"vassili\": {\"nuggets\": 55.5}," + + "\"tony\": {\"nuggets\": 52}" + + "}"); + + Query query = ref.orderByChild("nuggets"); + + final Semaphore semaphore = new Semaphore(0); + final String[] prevName = new String[1]; + final DataSnapshot[] snapshot = new DataSnapshot[1]; + final ChildEventListener testListener = + query.addChildEventListener( + new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) {} + + @Override + public void onChildMoved(DataSnapshot snap, String previousChildName) { + snapshot[0] = snap; + prevName[0] = previousChildName; + semaphore.release(); + } + + @Override + public void onChildChanged(DataSnapshot snap, String previousChildName) {} + }); + + ref.setValue(initial); + ref.child("greg/nuggets").setValue(57); + + TestHelpers.waitFor(semaphore); + + Assert.assertEquals("greg", snapshot[0].getKey()); + Assert.assertEquals("rob", prevName[0]); + Map expectedValue = MapBuilder.of("nuggets", 57L); + Assert.assertEquals(expectedValue, snapshot[0].getValue()); + + TestHelpers.waitForRoundtrip(ref); + ref.removeEventListener(testListener); + } + + @Test + public void testMultipleIndexesAtLocation() + throws IOException, ExecutionException, InterruptedException, TimeoutException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + Map foo1 = + TestHelpers.fromJsonString( + "{" + + "\"a\": {\"order\": 2, \"foo\": 2}," + + "\"b\": {\"order\": 0}," + + "\"c\": {\"order\": 1, \"foo\": false}," + + "\"d\": {\"order\": 3, \"foo\": \"hello\"}" + + "}"); + + String indexDef = "{\".indexOn\": [\"order\", \"foo\"]}"; + String rules = formatRules(reader, indexDef); + uploadRules(masterApp, rules); + + new WriteFuture(writer, foo1).timedGet(); + + Query fooQuery = reader.orderByChild("foo"); + Query orderQuery = reader.orderByChild("order"); + final List fooSnaps = new ArrayList<>(); + final List orderSnaps = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + + final ValueEventListener fooListener = + fooQuery + .startAt(null) + .endAt(1) + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + fooSnaps.add(snapshot); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + final ValueEventListener orderListener = + orderQuery + .limitToLast(2) + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + orderSnaps.add(snapshot); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + TestHelpers.waitFor(semaphore, 2); + + Map fooExpected = new HashMap<>(); + fooExpected.put("b", MapBuilder.of("order", 0L)); + fooExpected.put("c", MapBuilder.of("order", 1L, "foo", false)); + + Map orderExpected = new HashMap<>(); + orderExpected.put("d", MapBuilder.of("order", 3L, "foo", "hello")); + orderExpected.put("a", MapBuilder.of("order", 2L, "foo", 2L)); + + Assert.assertEquals(1, fooSnaps.size()); + Assert.assertEquals(fooExpected, fooSnaps.get(0).getValue()); + Assert.assertEquals(1, orderSnaps.size()); + Assert.assertEquals(orderExpected, orderSnaps.get(0).getValue()); + + new WriteFuture(writer.child("a"), MapBuilder.of("order", -1L, "foo", 1L)).timedGet(); + + fooExpected = new HashMap<>(); + fooExpected.put("a", MapBuilder.of("order", -1L, "foo", 1L)); + fooExpected.put("b", MapBuilder.of("order", 0L)); + fooExpected.put("c", MapBuilder.of("order", 1L, "foo", false)); + + orderExpected = new HashMap<>(); + orderExpected.put("d", MapBuilder.of("order", 3L, "foo", "hello")); + orderExpected.put("c", MapBuilder.of("order", 1L, "foo", false)); + + TestHelpers.waitFor(semaphore, 2); + + Assert.assertEquals(2, fooSnaps.size()); + Assert.assertEquals(fooExpected, fooSnaps.get(1).getValue()); + + //FIXME: There are 3 snapshots here for some reason, instead of 2. + //Assert.assertEquals(2, orderSnaps.size()); + //Assert.assertEquals(orderExpected, orderSnaps.get(1).getValue()); + + // cleanup + TestHelpers.waitForRoundtrip(reader); + reader.removeEventListener(fooListener); + reader.removeEventListener(orderListener); + } + + @Test + public void testCallbackRemoval() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + final int[] reads = new int[1]; + final Semaphore semaphore = new Semaphore(0); + final ValueEventListener fooListener = + ref.orderByChild("foo") + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + reads[0]++; + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + final ValueEventListener barListener = + ref.orderByChild("bar") + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + reads[0]++; + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + final ValueEventListener bazListener = + ref.orderByChild("baz") + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + reads[0]++; + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + final ValueEventListener defaultListener = + ref.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + reads[0]++; + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + // wait for initial null events. + TestHelpers.waitFor(semaphore, 4); + Assert.assertEquals(4, reads[0]); + + ref.setValue(1); + + TestHelpers.waitFor(semaphore, 4); + Assert.assertEquals(8, reads[0]); + + ref.removeEventListener(fooListener); + ref.setValue(2); + + TestHelpers.waitFor(semaphore, 3); + Assert.assertEquals(11, reads[0]); + + // Should be a no-op resulting in 3 more reads + ref.orderByChild("foo").removeEventListener(bazListener); + ref.setValue(3); + + TestHelpers.waitFor(semaphore, 3); + Assert.assertEquals(14, reads[0]); + + ref.orderByChild("bar").removeEventListener(barListener); + ref.setValue(4); + TestHelpers.waitFor(semaphore, 2); + Assert.assertEquals(16, reads[0]); + + // Now, remove everything + ref.removeEventListener(bazListener); + ref.removeEventListener(defaultListener); + ref.setValue(5); + + // No more reads + TestHelpers.waitForRoundtrip(ref); + Assert.assertEquals(16, reads[0]); + } + + @Test + public void testChildAddedEvents() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + Map initial = + new MapBuilder() + .put("a", MapBuilder.of("value", 5L)) + .put("c", MapBuilder.of("value", 3L)) + .build(); + + final List snapshotNames = new ArrayList<>(); + final List prevNames = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener testListener = + ref.orderByChild("value") + .addChildEventListener( + new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snap, String prevName) { + snapshotNames.add(snap.getKey()); + prevNames.add(prevName); + semaphore.release(); + } + }); + + ref.setValue(initial); + TestHelpers.waitFor(semaphore, 2); + Assert.assertEquals(Arrays.asList("c", "a"), snapshotNames); + Assert.assertEquals(Arrays.asList(null, "c"), prevNames); + + Map updates = new HashMap<>(); + updates.put("b", MapBuilder.of("value", 4)); + updates.put("d", MapBuilder.of("value", 2)); + ref.updateChildren(updates); + + TestHelpers.waitFor(semaphore, 2); + Assert.assertEquals(Arrays.asList("c", "a", "d", "b"), snapshotNames); + Assert.assertEquals(Arrays.asList(null, "c", null, "c"), prevNames); + ref.removeEventListener(testListener); + } + + @Test + public void testUpdatesForUnindexedQuery() + throws InterruptedException, ExecutionException, TestFailure, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference reader = refs.get(0); + final DatabaseReference writer = refs.get(1); + + final List snapshots = new ArrayList<>(); + + Map value = new HashMap<>(); + value.put("one", new MapBuilder().put("index", 1).put("value", "one").build()); + value.put("two", new MapBuilder().put("index", 2).put("value", "two").build()); + value.put("three", new MapBuilder().put("index", 3).put("value", "three").build()); + + new WriteFuture(writer, value).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + + Query query = reader.orderByChild("index").limitToLast(2); + final ValueEventListener listener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + snapshots.add(snapshot); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + TestHelpers.waitFor(semaphore); + + Assert.assertEquals(1, snapshots.size()); + + Map expected1 = new HashMap<>(); + expected1.put("two", new MapBuilder().put("index", 2L).put("value", "two").build()); + expected1.put("three", new MapBuilder().put("index", 3L).put("value", "three").build()); + Assert.assertEquals(expected1, snapshots.get(0).getValue()); + + // update child which should trigger value event + writer.child("one/index").setValue(4); + TestHelpers.waitFor(semaphore); + + Assert.assertEquals(2, snapshots.size()); + Map expected2 = new HashMap<>(); + expected2.put("three", new MapBuilder().put("index", 3L).put("value", "three").build()); + expected2.put("one", new MapBuilder().put("index", 4L).put("value", "one").build()); + Assert.assertEquals(expected2, snapshots.get(1).getValue()); + + // cleanup + TestHelpers.waitForRoundtrip(reader); + reader.removeEventListener(listener); + } + + @Test + public void testQueriesOnLeafNodes() + throws InterruptedException, ExecutionException, TestFailure, TimeoutException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + final Semaphore semaphore = new Semaphore(0); + new WriteFuture(ref, "leaf-node").timedGet(); + + final List snapshots = new ArrayList<>(); + Query query = ref.orderByChild("foo").limitToLast(1); + final ValueEventListener listener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + snapshots.add(snapshot); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + TestHelpers.waitFor(semaphore); + + Assert.assertEquals(1, snapshots.size()); + Assert.assertNull(snapshots.get(0).getValue()); + + // cleanup + TestHelpers.waitForRoundtrip(ref); + ref.removeEventListener(listener); + } + + @Test + public void testServerRespectsKeyIndex() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + Map initial = MapBuilder.of("a", 1, "b", 2, "c", 3); + // If the server doesn't respect the index, it will send down limited data, but with no + // offset, so the expected and actual data don't match. + Query query = reader.orderByKey().startAt("b").limitToFirst(2); + + new WriteFuture(writer, initial).timedGet(); + + final List actualChildren = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + ValueEventListener valueListener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + for (DataSnapshot child : snapshot.getChildren()) { + actualChildren.add(child.getKey()); + } + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + TestHelpers.waitFor(semaphore); + Assert.assertEquals(ImmutableList.of("b", "c"), actualChildren); + + // cleanup + reader.removeEventListener(valueListener); + } + + @Test + public void testServerRespectsValueIndex() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final String indexRule = "{ \".indexOn\": \".value\"}"; + String rules = formatRules(writer, indexRule); + + uploadRules(masterApp, rules); + + Map initial = MapBuilder.of("a", 1, "c", 2, "b", 3); + // If the server doesn't respect the index, it will send down limited data, but with no + // offset, so the expected and actual data don't match. + Query query = reader.orderByValue().startAt(2).limitToFirst(2); + + new WriteFuture(writer, initial).timedGet(); + + final List actualChildren = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + ValueEventListener valueListener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + for (DataSnapshot child : snapshot.getChildren()) { + actualChildren.add(child.getKey()); + } + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + TestHelpers.waitFor(semaphore); + + Assert.assertEquals(ImmutableList.of("c", "b"), actualChildren); + + // cleanup + reader.removeEventListener(valueListener); + } + + @Test + public void testDeepUpdates() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final String indexRule = "{ \".indexOn\": \"idx\"}"; + String rules = formatRules(writer, indexRule); + + uploadRules(masterApp, rules); + + Map initial = + new MapBuilder() + .put("a", MapBuilder.of("data", "foo", "idx", true)) + .put("b", MapBuilder.of("data", "bar", "idx", true)) + .put("c", MapBuilder.of("data", "baz", "idx", false)) + .build(); + new WriteFuture(writer, initial).timedGet(); + + Query query = reader.orderByChild("idx").equalTo(true); + + DataSnapshot snap = TestHelpers.getSnap(query); + TestHelpers.assertDeepEquals( + new MapBuilder() + .put("a", MapBuilder.of("data", "foo", "idx", true)) + .put("b", MapBuilder.of("data", "bar", "idx", true)) + .build(), + snap.getValue()); + + Map update = + new MapBuilder().put("a/idx", false).put("b/data", "blah").put("c/idx", true).build(); + final Semaphore semaphore = new Semaphore(0); + writer.updateChildren( + update, + new CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(1); + } + }); + TestHelpers.waitFor(semaphore); + + snap = TestHelpers.getSnap(query); + TestHelpers.assertDeepEquals( + new MapBuilder() + .put("b", MapBuilder.of("data", "blah", "idx", true)) + .put("c", MapBuilder.of("data", "baz", "idx", true)) + .build(), + snap.getValue()); + } + + @Test + public void testStartAtAndEndAtOnValueIndex() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + Map initial = + TestHelpers.fromJsonString( + "{" + + "\"alex\": 60," + + "\"greg\": 52," + + "\"rob\": 56," + + "\"vassili\": 55.5," + + "\"tony\": 52" + + "}"); + + Query query = ref.orderByValue().startAt(52, "tony").endAt(56); + + final List valueOrder = new ArrayList<>(); + final List childOrder = new ArrayList<>(); + final List childPrevNames = new ArrayList<>(); + final ValueEventListener valueListener = + query.addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + for (DataSnapshot child : snapshot.getChildren()) { + valueOrder.add(child.getKey()); + } + } + + @Override + public void onCancelled(DatabaseError error) { + Assert.fail(); + } + }); + + final ChildEventListener testListener = + query.addChildEventListener( + new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + childOrder.add(snapshot.getKey()); + childPrevNames.add(previousChildName); + } + }); + + new WriteFuture(ref, initial).timedGet(); + + List expectedOrder = Arrays.asList("tony", "vassili", "rob"); + List expectedPrevNames = Arrays.asList(null, "tony", "vassili"); + Assert.assertEquals(expectedOrder, valueOrder); + Assert.assertEquals(expectedOrder, childOrder); + Assert.assertEquals(expectedPrevNames, childPrevNames); + + // cleanup + TestHelpers.waitForRoundtrip(ref); + ref.removeEventListener(testListener); + ref.removeEventListener(valueListener); + } + + @Test + public void testRemovingDefaultListener() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure, IOException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp) ; + + Object initialData = MapBuilder.of("key", "value"); + new WriteFuture(ref, initialData).timedGet(); + + ValueEventListener listener = + ref.orderByKey() + .addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) {} + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.addValueEventListener(listener); + // Should remove both listener and should remove the listen sent to the server + ref.removeEventListener(listener); + + // This used to crash because a listener for ref.orderByKey() existed already + Object result = new ReadFuture(ref.orderByKey()).waitForLastValue(); + assertEquals(initialData, result); + } +} diff --git a/src/test/java/com/google/firebase/database/integration/OrderTestIT.java b/src/test/java/com/google/firebase/database/integration/OrderTestIT.java new file mode 100644 index 000000000..c318a4cec --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/OrderTestIT.java @@ -0,0 +1,824 @@ +package com.google.firebase.database.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.core.view.Event; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.testing.IntegrationTestUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class OrderTestIT { + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() throws TestFailure, TimeoutException, InterruptedException { + masterApp = IntegrationTestUtils.initDefaultApp(); + // Make sure we're connected before any of these tests run + DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference(); + ReadFuture.untilEquals(ref.child(".info/connected"), true).timedGet(); + } + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testPushAndEnumerate() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + for (int i = 0; i < 10; ++i) { + ref.push().setValue(i); + } + + DataSnapshot snap = new ReadFuture(ref).timedGet().get(0).getSnapshot(); + + long i = 0; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i++; + } + + assertEquals(10L, i); + } + + @Test + public void testPushWriteAndEnumerate() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + List paths = new ArrayList<>(20); + // Generate children quickly to try to get a few in the same millisecond + for (int i = 0; i < 20; ++i) { + paths.add(ref.push()); + } + + for (int i = 0; i < paths.size(); ++i) { + paths.get(i).setValue(i); + } + + DataSnapshot snap = new ReadFuture(ref).timedGet().get(0).getSnapshot(); + + long i = 0; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i++; + } + + assertEquals(20L, i); + } + + @Test + public void testReconnectAndRead() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + for (int i = 0; i < 9; ++i) { + writer.push().setValue(i); + } + new WriteFuture(writer.push(), 9).timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(writer); + long i = 0; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i++; + } + assertEquals(10L, i); + + snap = TestHelpers.getSnap(reader); + i = 0; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i++; + } + assertEquals(10L, i); + } + + @Test + public void testReconnectAndReadWithPriority() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + for (int i = 0; i < 9; ++i) { + writer.push().setValue(i, 10 - i); + } + new WriteFuture(writer.push(), 9, 1).timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(writer); + long i = 9; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i--; + } + assertEquals(-1L, i); + + snap = TestHelpers.getSnap(reader); + i = 9; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i--; + } + assertEquals(-1L, i); + } + + @Test + public void testExponentialPriority() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + for (int i = 0; i < 9; ++i) { + writer.push().setValue(i, 111111111111111111111111111111.0 / Math.pow(10, i)); + } + new WriteFuture(writer.push(), 9, 111111111111111111111111111111.0 / Math.pow(10, 9)) + .timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(writer); + long i = 9; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i--; + } + assertEquals(-1L, i); + + snap = TestHelpers.getSnap(reader); + i = 9; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i--; + } + assertEquals(-1L, i); + } + + @Test + public void testEnumerateNodesWithoutValues() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.child("foo"); + ref.child("bar").setValue("test"); + + DataSnapshot snap = TestHelpers.getSnap(ref); + int i = 0; + for (DataSnapshot child : snap.getChildren()) { + i++; + assertEquals("bar", child.getKey()); + } + assertEquals(1, i); + } + + @Test + public void testChildEventsOnPriorityChanges() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final EventHelper helper = + new EventHelper() + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "a") + .addValueExpectation(ref) + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "b") + .addValueExpectation(ref) + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "c") + .addValueExpectation(ref) + .addChildExpectation(ref, Event.EventType.CHILD_MOVED, "a") + .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "a") + .addValueExpectation(ref) + .startListening(true); + + ref.child("a").setValue("first", 1); + ref.child("b").setValue("second", 5); + ref.child("c").setValue("third", 10); + ref.child("a").setPriority(15); + + assertTrue(helper.waitForEvents()); + helper.cleanup(); + } + + @Test + public void testResetPriorityToNull() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.child("a").setValue("a", 1); + ref.child("b").setValue("b", 2); + + TestHelpers.waitForRoundtrip(ref); + EventHelper helper = + new EventHelper() + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "a") + .addChildExpectation(ref, Event.EventType.CHILD_ADDED, "b") + .addValueExpectation(ref) + .startListening(); + + assertTrue(helper.waitForEvents()); + + helper + .addChildExpectation(ref, Event.EventType.CHILD_MOVED, "b") + .addChildExpectation(ref, Event.EventType.CHILD_CHANGED, "b") + .addValueExpectation(ref) + .startListening(); + + ref.child("b").setPriority(null); + assertTrue(helper.waitForEvents()); + + DataSnapshot snap = TestHelpers.getSnap(ref); + assertNull(snap.child("b").getPriority()); + helper.cleanup(); + } + + @Test + public void testInsertingNodeUnderLeaf() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture readFuture = ReadFuture.untilCountAfterNull(ref, 2); + + ref.setValue("a", 10); + ref.child("deeper").setValue("deeper"); + + DataSnapshot snap = readFuture.timedGet().get(1).getSnapshot(); + assertEquals(10.0, snap.getPriority()); + } + + @Test + public void testMixedPriorityTypes() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + final DatabaseReference reader = refs.get(1); + + writer.child("alpha42").setValue(1, "zed"); + writer.child("noPriorityC").setValue(1, (String) null); + writer.child("num41").setValue(1, 500); + writer.child("noPriorityB").setValue(1, (String) null); + writer.child("num80").setValue(1, 4000.1); + writer.child("num50").setValue(1, 4000); + writer.child("num10").setValue(1, 24); + writer.child("alpha41").setValue(1, "zed"); + writer.child("alpha20").setValue(1, "horse"); + writer.child("num20").setValue(1, 123); + writer.child("num70").setValue(1, 4000.01); + writer.child("noPriorityA").setValue(1, (String) null); + writer.child("alpha30").setValue(1, "tree"); + writer.child("num30").setValue(1, 300); + writer.child("num60").setValue(1, 4000.001); + writer.child("alpha10").setValue(1, "0horse"); + writer.child("num42").setValue(1, 500); + writer.child("alpha40").setValue(1, "zed"); + new WriteFuture(writer.child("num40"), 1, 500).timedGet(); + + List expected = ImmutableList.builder().add( + "noPriorityA", + "noPriorityB", + "noPriorityC", + "num10", + "num20", + "num30", + "num40", + "num41", + "num42", + "num50", + "num60", + "num70", + "num80", + "alpha10", + "alpha20", + "alpha30", + "alpha40", + "alpha41", + "alpha42").build(); + + List actual = new ArrayList<>(expected.size()); + DataSnapshot snap = TestHelpers.getSnap(writer); + for (DataSnapshot child : snap.getChildren()) { + actual.add(child.getKey()); + } + TestHelpers.assertDeepEquals(expected, actual); + + actual.clear(); + snap = TestHelpers.getSnap(reader); + for (DataSnapshot child : snap.getChildren()) { + actual.add(child.getKey()); + } + TestHelpers.assertDeepEquals(expected, actual); + } + + @Test + public void testIntegerKeyOrder() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + + writer.child("foo").setValue(0); + writer.child("bar").setValue(0); + writer.child("03").setValue(0); + writer.child("0").setValue(0); + writer.child("100").setValue(0); + writer.child("20").setValue(0); + writer.child("5").setValue(0); + writer.child("3").setValue(0); + writer.child("003").setValue(0); + new WriteFuture(writer.child("9"), 0).timedGet(); + + List expected = ImmutableList.of( + "0", "3", "03", "003", "5", "9", "20", "100", "bar", "foo"); + List actual = new ArrayList<>(expected.size()); + DataSnapshot snap = TestHelpers.getSnap(writer); + for (DataSnapshot child : snap.getChildren()) { + actual.add(child.getKey()); + } + TestHelpers.assertDeepEquals(expected, actual); + } + + @Test + public void testLargeIntegerKeyOrder() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + + writer.child("2000000000").setValue(0); + new WriteFuture(writer.child("-2000000000"), 0).timedGet(); + + List expected = ImmutableList.of("-2000000000", "2000000000"); + List actual = new ArrayList<>(expected.size()); + DataSnapshot snap = TestHelpers.getSnap(writer); + for (DataSnapshot child : snap.getChildren()) { + actual.add(child.getKey()); + } + TestHelpers.assertDeepEquals(expected, actual); + } + + @Test + public void testPrevNameOnChildAddedEvent() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List results = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + results.add(snapshot.getKey()); + results.add(previousChildName); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + fail("Should not happen"); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + fail("Should not happen"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + fail("Should not happen"); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.setValue(new MapBuilder().put("a", 1).put("b", 2).put("c", 3).build()); + + TestHelpers.waitFor(semaphore, 3); + List expected = Arrays.asList("a", null, "b", "a", "c", "b"); + TestHelpers.assertDeepEquals(expected, results); + ref.removeEventListener(listener); + } + + @Test + public void testPrevNameOnAddingNewNodes() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List results = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + results.add(snapshot.getKey()); + results.add(previousChildName); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + fail("Should not happen"); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + fail("Should not happen"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + fail("Should not happen"); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.setValue(MapBuilder.of("b", 2, "c", 3, "d", 4)); + + ref.child("a").setValue(1); + ref.child("e").setValue(5); + + TestHelpers.waitFor(semaphore, 5); + List expected = Arrays.asList("b", null, "c", "b", "d", "c", "a", null, "e", "d"); + TestHelpers.assertDeepEquals(expected, results); + ref.removeEventListener(listener); + } + + @Test + public void testPrevNameOnAddingNewNodesWithJSON() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List results = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + results.add(snapshot.getKey()); + results.add(previousChildName); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + fail("Should not happen"); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + fail("Should not happen"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + fail("Should not happen"); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.setValue(MapBuilder.of("b", 2, "c", 3, "d", 4)); + ref.setValue(new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4).build()); + ref.setValue(new MapBuilder().put("a", 1).put("b", 2).put("c", 3) + .put("d", 4).put("e", 5).build()); + + TestHelpers.waitFor(semaphore, 5); + List expected = Arrays.asList("b", null, "c", "b", "d", "c", "a", null, "e", "d"); + TestHelpers.assertDeepEquals(expected, results); + ref.removeEventListener(listener); + } + + @Test + public void testPrevNameIOnMovingNodes() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List results = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + results.add("CHANGED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + fail("Should not happen"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + results.add("MOVED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.child("a").setValue("a", 1); + ref.child("b").setValue("b", 2); + ref.child("c").setValue("c", 3); + ref.child("d").setValue("d", 4); + + ref.child("d").setPriority(0); + + ref.child("a").setPriority(4); + + ref.child("c").setPriority(0.5); + + TestHelpers.waitFor(semaphore, 6); + + List expected = ImmutableList.of("MOVED:d/null", "CHANGED:d/null", "MOVED:a/c", + "CHANGED:a/c", "MOVED:c/d", "CHANGED:c/d"); + TestHelpers.assertDeepEquals(expected, results); + ref.removeEventListener(listener); + } + + @Test + public void testPrevNameOnWhenMovingNodesBySettingJson() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List results = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + results.add("CHANGED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + fail("Should not happen"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + results.add("MOVED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.setValue( + new MapBuilder() + .put("a", new MapBuilder().put(".value", "a").put(".priority", 1).build()) + .put("b", new MapBuilder().put(".value", "b").put(".priority", 2).build()) + .put("c", new MapBuilder().put(".value", "c").put(".priority", 3).build()) + .put("d", new MapBuilder().put(".value", "d").put(".priority", 4).build()) + .build()); + + ref.setValue( + new MapBuilder() + .put("d", new MapBuilder().put(".value", "d").put(".priority", 0).build()) + .put("a", new MapBuilder().put(".value", "a").put(".priority", 1).build()) + .put("b", new MapBuilder().put(".value", "b").put(".priority", 2).build()) + .put("c", new MapBuilder().put(".value", "c").put(".priority", 3).build()) + .build()); + + ref.setValue( + new MapBuilder() + .put("d", new MapBuilder().put(".value", "d").put(".priority", 0).build()) + .put("b", new MapBuilder().put(".value", "b").put(".priority", 2).build()) + .put("c", new MapBuilder().put(".value", "c").put(".priority", 3).build()) + .put("a", new MapBuilder().put(".value", "a").put(".priority", 4).build()) + .build()); + + ref.setValue( + new MapBuilder() + .put("d", new MapBuilder().put(".value", "d").put(".priority", 0).build()) + .put("c", new MapBuilder().put(".value", "c").put(".priority", 0.5).build()) + .put("b", new MapBuilder().put(".value", "b").put(".priority", 2).build()) + .put("a", new MapBuilder().put(".value", "a").put(".priority", 4).build()) + .build()); + + TestHelpers.waitFor(semaphore, 6); + + List expected = ImmutableList.of("MOVED:d/null", "CHANGED:d/null", "MOVED:a/c", + "CHANGED:a/c", "MOVED:c/d", "CHANGED:c/d"); + TestHelpers.assertDeepEquals(expected, results); + ref.removeEventListener(listener); + } + + @Test + public void testCase595DeletingPrioritizedGrandChild() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + fail("Should not happen"); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.child("test/foo").setValue(42, "5"); + ref.child("test/f002").setValue(42, "10"); + ref.child("test/foo").removeValue(); + new WriteFuture(ref.child("test/foo2"), null).timedGet(); + // If child_moved has been raised, the test will have failed by now + ref.removeEventListener(listener); + } + + @Test + public void testSetValuePriorityToZero() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture readFuture = new ReadFuture(ref); + ref.setValue("test", 0); + + DataSnapshot snap = readFuture.timedGet().get(0).getSnapshot(); + + assertEquals(0.0, snap.getPriority()); + } + + @Test + public void testSetObjectPriorityToZero() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ReadFuture readFuture = new ReadFuture(ref); + ref.setValue(new MapBuilder().put("x", "test").put("y", 7).build(), 0); + + DataSnapshot snap = readFuture.timedGet().get(0).getSnapshot(); + + assertEquals(0.0, snap.getPriority()); + } + + @Test + public void testCase2003ChildMovedForPriorityChange1() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List results = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + results.add("CHANGED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + fail("Should not happen"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + results.add("MOVED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) {} + }); + + ref.setValue( + new MapBuilder() + .put("a", new MapBuilder().put(".value", "a").put(".priority", 0).build()) + .put("b", new MapBuilder().put(".value", "b").put(".priority", 1).build()) + .put("c", new MapBuilder().put(".value", "c").put(".priority", 2).build()) + .put("d", new MapBuilder().put(".value", "d").put(".priority", 3).build()) + .build()); + + ref.child("b").setPriority(1.5); + TestHelpers.waitFor(semaphore, 2); + + assertEquals(2, results.size()); + assertEquals(ImmutableList.of("MOVED:b/a", "CHANGED:b/a"), results); + ref.removeEventListener(listener); + } + + @Test + public void testCase2003ChildMovedForPriorityChange2() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List results = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = + ref.addChildEventListener( + new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + results.add("CHANGED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + fail("Should not happen"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + results.add("MOVED:" + snapshot.getKey() + "/" + previousChildName); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not happen"); + } + }); + + ref.setValue( + new MapBuilder() + .put("a", new MapBuilder().put(".value", "a").put(".priority", 0).build()) + .put("b", new MapBuilder().put(".value", "b").put(".priority", 1).build()) + .put("c", new MapBuilder().put(".value", "c").put(".priority", 2).build()) + .put("d", new MapBuilder().put(".value", "d").put(".priority", 3).build()) + .build()); + + ref.setValue( + new MapBuilder() + .put("a", new MapBuilder().put(".value", "a").put(".priority", 0).build()) + .put("b", new MapBuilder().put(".value", "b").put(".priority", 1.5).build()) + .put("c", new MapBuilder().put(".value", "c").put(".priority", 2).build()) + .put("d", new MapBuilder().put(".value", "d").put(".priority", 3).build()) + .build()); + + TestHelpers.waitFor(semaphore, 2); + + assertEquals(2, results.size()); + assertEquals(ImmutableList.of("MOVED:b/a", "CHANGED:b/a"), results); + ref.removeEventListener(listener); + } +} diff --git a/src/test/java/com/google/firebase/database/integration/QueryTestIT.java b/src/test/java/com/google/firebase/database/integration/QueryTestIT.java new file mode 100644 index 000000000..5ad4bcaff --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/QueryTestIT.java @@ -0,0 +1,2878 @@ +package com.google.firebase.database.integration; + +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.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseException; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.Query; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.ValueExpectationHelper; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.testing.IntegrationTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import java.util.concurrent.atomic.AtomicLong; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class QueryTestIT { + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() throws IOException { + masterApp = IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testCreateBasicQueries() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + // Just make sure they don't throw anything + ref.limitToLast(10); + ref.startAt("199").limitToLast(10); + ref.startAt("199", "test").limitToLast(10); + ref.endAt(199).limitToLast(1); + ref.startAt(50, "test").endAt(100, "tree"); + ref.startAt(4).endAt(10); + ref.startAt(null).endAt(10); + ref.orderByChild("child"); + ref.orderByChild("child/deep/path"); + ref.orderByValue(); + ref.orderByPriority(); + } + + @Test + public void testInvalidPathsToOrderBy() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + List badKeys = ImmutableList.builder() + .add("$child/foo", "$childfoo", "$/foo", "$child/foo/bar", "$child/.foo", ".priority", + "$priority", "$key", ".key", "$child/.priority") + .build(); + for (String key : badKeys) { + try { + ref.orderByChild(key); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + } + } + + @Test + public void testInvalidQueries() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + try { + ref.orderByKey().orderByPriority(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().orderByValue(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().orderByChild("foo"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByValue().orderByPriority(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByValue().orderByKey(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByValue().orderByValue(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByValue().orderByChild("foo"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByChild("foo").orderByPriority(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByChild("foo").orderByKey(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByChild("foo").orderByValue(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().startAt(1); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().startAt(null); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().endAt(null); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().equalTo(null); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().startAt("test", "test"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().endAt(1); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().endAt("test", "test"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByKey().orderByPriority(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByPriority().orderByKey(); + fail("Should throw"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByPriority().orderByValue(); + fail("Should throw"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByPriority().orderByPriority(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.limitToLast(1).limitToLast(1); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.limitToFirst(1).limitToLast(1); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.limitToLast(1).limitToFirst(1); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.equalTo(true).endAt("test", "test"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.equalTo(true).startAt("test", "test"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.equalTo(true).equalTo("test", "test"); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.equalTo("test").equalTo(true); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByChild("foo").orderByKey(); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.limitToFirst(5).limitToLast(10); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.startAt(5).equalTo(10); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByPriority().startAt(false); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByPriority().endAt(true); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + try { + ref.orderByPriority().equalTo(true); + fail("Should throw"); + } catch (DatabaseException | IllegalArgumentException e) { // ignore + } + } + + @Test + public void testInvalidKeysInStartAtOrEndAtQueries() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + List badKeys = ImmutableList.of(".test", "test.", "fo$o", "[what", "ever]", "ha#sh", + "/thing", "th/ing", "thing/"); + for (String key : badKeys) { + try { + ref.startAt(null, key); + fail("Should throw"); + } catch (DatabaseException e) { // ignore + + } + + try { + ref.endAt(null, key); + fail("Should throw"); + } catch (DatabaseException e) { // ignore + + } + } + } + + @Test + public void testRemoveListener() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + ValueEventListener listener = ref.limitToLast(5) + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.setValue(MapBuilder.of("a", 5, "b", 6)); + TestHelpers.waitFor(semaphore, 1); + ref.limitToLast(5).removeEventListener(listener); + new WriteFuture(ref, MapBuilder.of("a", 6, "b", 5)).timedGet(); + TestHelpers.waitForQueue(ref); + + assertEquals(0, semaphore.availablePermits()); + } + + @Test + public void testLimitQuery() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue(new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4) + .put("e", 5).put("f", 6).build()); + + DataSnapshot snap = new ReadFuture(ref.limitToLast(5)).timedGet().get(0).getSnapshot(); + + assertEquals(5, snap.getChildrenCount()); + assertFalse(snap.hasChild("a")); + assertTrue(snap.hasChild("b")); + assertTrue(snap.hasChild("f")); + } + + @Test + public void testLimitQueryServerResponse() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + for (int i = 0; i < 9; ++i) { + ref.push().setValue(i); + } + new WriteFuture(ref.push(), 9).timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(ref.limitToLast(5)); + + long i = 5; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(i, child.getValue()); + i++; + } + + assertEquals(10L, i); + } + + @Test + public void testMultipleLimitQueries() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ValueExpectationHelper expectations = new ValueExpectationHelper(); + expectations.add(ref.limitToLast(1), MapBuilder.of("c", 3L)); + expectations.add(ref.endAt(null).limitToLast(1), MapBuilder.of("c", 3L)); + expectations.add(ref.limitToLast(2), MapBuilder.of("b", 2L, "c", 3L)); + expectations.add(ref.limitToLast(3), + MapBuilder.of("a", 1L, "b", 2L, "c", 3L)); + expectations.add(ref.limitToLast(4), + MapBuilder.of("a", 1L, "b", 2L, "c", 3L)); + + ref.setValue(MapBuilder.of("a", 1L, "b", 2L, "c", 3L)); + + expectations.waitForEvents(); + } + + @Test + public void testMultipleLimitQueriesWithStartAt() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ValueExpectationHelper expectations = new ValueExpectationHelper(); + expectations.add(ref.startAt(null).limitToFirst(1), + MapBuilder.of("a", 1L)); + expectations.add(ref.startAt(null, "c").limitToFirst(1), + MapBuilder.of("c", 3L)); + expectations.add(ref.startAt(null, "b").limitToFirst(1), + MapBuilder.of("b", 2L)); + expectations.add(ref.startAt(null, "b").limitToFirst(2), + MapBuilder.of("b", 2L, "c", 3L)); + expectations.add(ref.startAt(null, "b").limitToFirst(3), + MapBuilder.of("b", 2L, "c", 3L)); + + ref.setValue(MapBuilder.of("a", 1, "b", 2, "c", 3)); + expectations.waitForEvents(); + } + + @Test + public void testMultipleLimitQueriesWithStartAtUsingServerData() + throws InterruptedException, TestFailure, ExecutionException, TimeoutException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + // TODO: this test has race conditions. The listens are added sequentially, so we get a + // lot of partial data back from the server. This all correct, and we + // end up in the correct state, but it's still kinda weird. Consider having + // ValueExpectationHelper deal with initial state. + + new WriteFuture(ref, MapBuilder.of("a", 1L, "b", 2L, "c", 3L)).timedGet(); + + ValueExpectationHelper expectations = new ValueExpectationHelper(); + expectations.add(ref.startAt(null).limitToFirst(1), MapBuilder.of("a", 1L)); + expectations.add(ref.startAt(null, "c").limitToFirst(1), + MapBuilder.of("c", 3L)); + expectations.add(ref.startAt(null, "b").limitToFirst(1), + MapBuilder.of("b", 2L)); + expectations.add(ref.startAt(null, "b").limitToFirst(2), + MapBuilder.of("b", 2L, "c", 3L)); + expectations.add(ref.startAt(null, "b").limitToFirst(3), + MapBuilder.of("b", 2L, "c", 3L)); + + expectations.waitForEvents(); + } + + @Test + public void testLimitQueryChildEvents() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + + ref.limitToLast(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new WriteFuture(ref, MapBuilder.of("a", 1, "b", 2,"c", 3)).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), added); + assertTrue(removed.isEmpty()); + + added.clear(); + new WriteFuture(ref.child("d"), 4).timedGet(); + assertEquals(1, added.size()); + assertEquals("d", added.get(0)); + assertEquals(1, removed.size()); + assertEquals("b", removed.get(0)); + } + + @Test + public void testLimitQueryChildEventsUsingServerData() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + + new WriteFuture(ref, MapBuilder.of("a", 1,"b", 2,"c", 3)).timedGet(); + final Semaphore semaphore = new Semaphore(0); + ref.limitToLast(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore, 2); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), added); + assertTrue(removed.isEmpty()); + added.clear(); + + new WriteFuture(ref.child("d"), 4).timedGet(); + assertEquals(1, added.size()); + assertEquals("d", added.get(0)); + assertEquals(1, removed.size()); + assertEquals("b", removed.get(0)); + } + + @Test + public void testLimitQueryChildEventsWithStartAt() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + + ref.startAt(null, "a").limitToFirst(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new WriteFuture(ref, MapBuilder.of("a", 1,"b", 2,"c", 3)).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("a", "b"), added); + assertTrue(removed.isEmpty()); + added.clear(); + + new WriteFuture(ref.child("aa"), 4).timedGet(); + assertEquals(1, added.size()); + assertEquals("aa", added.get(0)); + assertEquals(1, removed.size()); + assertEquals("b", removed.get(0)); + } + + @Test + public void testLimitQueryChildEventsWithStartAtUsingServerData() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + + new WriteFuture(ref, MapBuilder.of("a", 1,"b", 2,"c", 3)).timedGet(); + final Semaphore semaphore = new Semaphore(0); + ref.startAt(null, "a").limitToFirst(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore, 2); + TestHelpers.assertDeepEquals(ImmutableList.of("a", "b"), added); + assertTrue(removed.isEmpty()); + + added.clear(); + new WriteFuture(ref.child("aa"), 4).timedGet(); + assertEquals(1, added.size()); + assertEquals("aa", added.get(0)); + assertEquals(1, removed.size()); + assertEquals("b", removed.get(0)); + } + + @Test + public void testLimitQueryUnderLimitChildEventsWithStartAt() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + + ref.startAt(null, "a").limitToFirst(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new WriteFuture(ref, MapBuilder.of("c", 3)).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("c"), added); + assertTrue(removed.isEmpty()); + added.clear(); + + new WriteFuture(ref.child("b"), 4).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("b"), added); + assertTrue(removed.isEmpty()); + } + + @Test + public void testLimitQueryUnderLimitChildEventsWithStartAtUsingServerData() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, MapBuilder.of("c", 3)).timedGet(); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + ref.startAt(null, "a").limitToFirst(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore); + TestHelpers.assertDeepEquals(ImmutableList.of("c"), added); + assertTrue(removed.isEmpty()); + added.clear(); + + new WriteFuture(ref.child("b"), 4).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("b"), added); + assertTrue(removed.isEmpty()); + } + + @Test + public void testSetLimitChildAddedAndChildRemovedWhenAnElementIsRemoved() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + + ref.limitToLast(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new WriteFuture(ref, MapBuilder.of("a", 1,"b", 2,"c", 3)).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), added); + assertTrue(removed.isEmpty()); + added.clear(); + + new WriteFuture(ref.child("b"), null).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("a"), added); + TestHelpers.assertDeepEquals(ImmutableList.of("b"), removed); + } + + @Test + public void testLimitQueryChildEventsWithNodeDeleteUsingServerData() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, MapBuilder.of("a", 1,"b", 2,"c", 3)).timedGet(); + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + + ref.limitToLast(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore, 2); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), added); + assertTrue(removed.isEmpty()); + + added.clear(); + new WriteFuture(ref.child("b"), null).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("a"), added); + TestHelpers.assertDeepEquals(ImmutableList.of("b"), removed); + } + + @Test + public void testSetLimitChildRemovedWhenAllElementsRemoved() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + + ref.limitToLast(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new WriteFuture(ref, MapBuilder.of("b", 2,"c", 3)).timedGet(); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), added); + assertTrue(removed.isEmpty()); + added.clear(); + + new WriteFuture(ref.child("b"), null).timedGet(); + assertTrue(added.isEmpty()); + TestHelpers.assertDeepEquals(ImmutableList.of("b"), removed); + + new WriteFuture(ref.child("c"), null).timedGet(); + assertTrue(added.isEmpty()); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), removed); + } + + @Test + public void testSetLimitChildRemovedWhenAllElementsRemovedUsingServerData() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, MapBuilder.of("b", 2,"c", 3)).timedGet(); + final List added = new ArrayList<>(); + final List removed = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + final ChildEventListener listener = ref.limitToLast(2) + .addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + added.add(snapshot.getKey()); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removed.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // no-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore, 2); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), added); + assertTrue(removed.isEmpty()); + + added.clear(); + new WriteFuture(ref.child("b"), null).timedGet(); + assertTrue(added.isEmpty()); + TestHelpers.assertDeepEquals(ImmutableList.of("b"), removed); + new WriteFuture(ref.child("c"), null).timedGet(); + assertTrue(added.isEmpty()); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c"), removed); + ref.limitToLast(2).removeEventListener(listener); + } + + @Test + public void testStartAtEndAtWithPriority() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ValueExpectationHelper helper = new ValueExpectationHelper(); + helper.add(ref.startAt("w").endAt("y"), + MapBuilder.of("b", 2L, "c", 3L, "d", 4L)); + helper.add(ref.startAt("w").endAt("w"), MapBuilder.of("d", 4L)); + helper.add(ref.startAt("a").endAt("c"), null); + + ref.setValue( + new MapBuilder() + .put("a", MapBuilder.of(".value", 1, ".priority", "z")) + .put("b", MapBuilder.of(".value", 2, ".priority", "y")) + .put("c", MapBuilder.of(".value", 3, ".priority", "x")) + .put("d", MapBuilder.of(".value", 4, ".priority", "w")).build()); + + helper.waitForEvents(); + } + + @Test + public void testStartAtEndAtWithPriorityUsingServerData() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue( + new MapBuilder() + .put("a", MapBuilder.of(".value", 1, ".priority", "z")) + .put("b", MapBuilder.of(".value", 2, ".priority", "y")) + .put("c", MapBuilder.of(".value", 3, ".priority", "x")) + .put("d", MapBuilder.of(".value", 4, ".priority", "w")).build()); + + ValueExpectationHelper helper = new ValueExpectationHelper(); + helper.add(ref.startAt("w").endAt("y"), + MapBuilder.of("b", 2L, "c", 3L, "d", 4L)); + helper.add(ref.startAt("w").endAt("w"), MapBuilder.of("d", 4L)); + helper.add(ref.startAt("a").endAt("c"), null); + + helper.waitForEvents(); + } + + @Test + public void testStartAtEndAtWithPriorityAndName() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ValueExpectationHelper helper = new ValueExpectationHelper(); + helper.add(ref.startAt(1, "a").endAt(2, "d"), + new MapBuilder().put("a", 1L).put("b", 2L).put("c", 3L).put("d", 4L).build()); + helper.add(ref.startAt(1, "b").endAt(2, "c"), + MapBuilder.of("b", 2L, "c", 3L)); + helper.add(ref.startAt(1, "c").endAt(2), + MapBuilder.of("c", 3L, "d", 4L)); + + ref.setValue( + new MapBuilder() + .put("a", MapBuilder.of(".value", 1, ".priority", 1)) + .put("b", MapBuilder.of(".value", 2, ".priority", 1)) + .put("c", MapBuilder.of(".value", 3, ".priority", 2)) + .put("d", MapBuilder.of(".value", 4, ".priority", 2)).build()); + + helper.waitForEvents(); + } + + @Test + public void testStartAtEndAtWithPriorityAndName2() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ValueExpectationHelper helper = new ValueExpectationHelper(); + helper.add(ref.startAt(1, "c").endAt(2, "b"), + new MapBuilder().put("a", 1L).put("b", 2L).put("c", 3L).put("d", 4L).build()); + helper.add(ref.startAt(1, "d").endAt(2, "a"), + MapBuilder.of("d", 4L, "a", 1L)); + helper.add(ref.startAt(1, "e").endAt(2), + MapBuilder.of("a", 1L, "b", 2L)); + + ref.setValue( + new MapBuilder() + .put("c", MapBuilder.of(".value", 3, ".priority", 1)) + .put("d", MapBuilder.of(".value", 4, ".priority", 1)) + .put("a", MapBuilder.of(".value", 1, ".priority", 2)) + .put("b", MapBuilder.of(".value", 2, ".priority", 2)).build()); + + helper.waitForEvents(); + } + + @Test + public void testStartAtEndAtWithPriorityAndNameUsingServerData() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue( + new MapBuilder() + .put("a", MapBuilder.of(".value", 1, ".priority", 1)) + .put("b", MapBuilder.of(".value", 2, ".priority", 1)) + .put("c", MapBuilder.of(".value", 3, ".priority", 2)) + .put("d", MapBuilder.of(".value", 4, ".priority", 2)).build()); + + ValueExpectationHelper helper = new ValueExpectationHelper(); + helper.add(ref.startAt(1, "a").endAt(2, "d"), + new MapBuilder().put("a", 1L).put("b", 2L).put("c", 3L).put("d", 4L).build()); + helper.add(ref.startAt(1, "b").endAt(2, "c"), + MapBuilder.of("b", 2L, "c", 3L)); + helper.add(ref.startAt(1, "c").endAt(2), + MapBuilder.of("c", 3L, "d", 4L)); + + helper.waitForEvents(); + } + + @Test + public void testStartAtEndAtWithPriorityAndNameUsingServerData2() throws + InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue( + new MapBuilder() + .put("c", MapBuilder.of(".value", 3, ".priority", 1)) + .put("d", MapBuilder.of(".value", 4, ".priority", 1)) + .put("a", MapBuilder.of(".value", 1, ".priority", 2)) + .put("b", MapBuilder.of(".value", 2, ".priority", 2)).build()); + + ValueExpectationHelper helper = new ValueExpectationHelper(); + helper.add(ref.startAt(1, "c").endAt(2, "b"), + new MapBuilder().put("a", 1L).put("b", 2L).put("c", 3L).put("d", 4L).build()); + helper.add(ref.startAt(1, "d").endAt(2, "a"), + MapBuilder.of("d", 4L, "a", 1L)); + helper.add(ref.startAt(1, "e").endAt(2), + MapBuilder.of("a", 1L, "b", 2L)); + + helper.waitForEvents(); + } + + @Test + public void testPrevNameWithLimit() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List names = new ArrayList<>(); + ref.limitToLast(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + names.add(snapshot.getKey() + " " + previousChildName); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.child("a").setValue(1); + ref.child("c").setValue(3); + ref.child("b").setValue(2); + new WriteFuture(ref.child("d"), 4).timedGet(); + + TestHelpers.assertDeepEquals(ImmutableList.of("a null", "c a", "b null", "d c"), names); + } + + // NOTE: skipping server data test here, it really doesn't test anything + // extra + + @Test + public void testSetLimitWithMoveNodes() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List names = new ArrayList<>(); + ref.limitToLast(2).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + names.add(snapshot.getKey() + " " + previousChildName); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.child("a").setValue("a", 10); + ref.child("b").setValue("b", 20); + ref.child("c").setValue("c", 30); + ref.child("d").setValue("d", 40); + + // Start moving things + ref.child("c").setPriority(50); + ref.child("c").setPriority(35); + new WriteFuture(ref.child("b"), "b", 33).timedGet(); + + TestHelpers.assertDeepEquals(ImmutableList.of("c d", "c null"), names); + } + + // NOTE: skipping server data version of the above test, it doesn't really + // test anything new + + // NOTE: skipping numeric priority test, the same functionality is tested + // above + + // NOTE: skipping local add / remove test w/ limits. Tested above + + @Test + public void testSetLimitWithAddNodesRemotely() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final List events = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + + ReadFuture future = new ReadFuture(reader); + final ChildEventListener listener = reader.limitToLast(2) + .addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + events.add(snapshot.getValue().toString() + " added"); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + events.add(snapshot.getValue().toString() + " removed"); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + // Wait for initial load + future.timedGet(); + + for (int i = 0; i < 4; ++i) { + writer.push().setValue(i); + } + new WriteFuture(writer.push(), 4).timedGet(); + List expected = ImmutableList.builder().add( + "0 added", "1 added", "0 removed", "2 added", "1 removed", + "3 added", "2 removed", "4 added").build(); + // Make sure we wait for all the events + TestHelpers.waitFor(semaphore, 5); + TestHelpers.assertDeepEquals(expected, events); + reader.limitToLast(2).removeEventListener(listener); + } + + @Test + public void testAttachingListener() { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ValueEventListener listener = ref.limitToLast(1) + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + assertNotNull(listener); + } + + @Test + public void testLimitOnUnsyncedNode() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + // This will timeout if value never fires + assertEquals(1, new ReadFuture(ref.limitToLast(1)).timedGet().size()); + } + + @Test + public void testFilteringOnlyNullPriorities() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue( + new MapBuilder().put("a", new MapBuilder().put(".priority", null).put(".value", 0).build()) + .put("b", new MapBuilder().put(".priority", null).put(".value", 1).build()) + .put("c", new MapBuilder().put(".priority", "2").put(".value", 2).build()) + .put("d", new MapBuilder().put(".priority", 3).put(".value", 3).build()) + .put("e", new MapBuilder().put(".priority", "hi").put(".value", 4).build()).build()); + + DataSnapshot snap = TestHelpers.getSnap(ref.endAt(null)); + Map expected = MapBuilder.of("a", 0L,"b", 1L); + Object result = snap.getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testNullPrioritiesIncludedInEndAt() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue( + new MapBuilder() + .put("a", MapBuilder.of(".priority", null, ".value", 0)) + .put("b", MapBuilder.of(".priority", null, ".value", 1)) + .put("c", MapBuilder.of(".priority", 2, ".value", 2)) + .put("d", MapBuilder.of(".priority", 3, ".value", 3)) + .put("e", MapBuilder.of(".priority", "hi", ".value", 4)).build()); + + DataSnapshot snap = TestHelpers.getSnap(ref.endAt(2)); + Map expected = MapBuilder.of("a", 0L, "b", 1L, "c", 2L); + Object result = snap.getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testNullPrioritiesIncludedInStartAt() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue( + new MapBuilder() + .put("a", new MapBuilder().put(".priority", null).put(".value", 0).build()) + .put("b", new MapBuilder().put(".priority", null).put(".value", 1).build()) + .put("c", new MapBuilder().put(".priority", 2).put(".value", 2).build()) + .put("d", new MapBuilder().put(".priority", 3).put(".value", 3).build()) + .put("e", new MapBuilder().put(".priority", "hi").put(".value", 4).build()).build()); + + DataSnapshot snap = TestHelpers.getSnap(ref.startAt(2)); + Object result = snap.getValue(); + Map expected = MapBuilder.of("c", 2L, "d", 3L, "e", 4L); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testLimitWithMixOfNullAndNonNullPrioritiesUsingServerData() throws + InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + Map toSet = new MapBuilder() + .put("Vikrum", + new MapBuilder().put(".priority", 1000.0).put("score", 1000L).put("name", "Vikrum") + .build()) + .put("Mike", + new MapBuilder().put(".priority", 500.0).put("score", 500L).put("name", "Mike").build()) + .put("Andrew", + new MapBuilder().put(".priority", 50.0).put("score", 50L).put("name", "Andrew").build()) + .put("James", + new MapBuilder().put(".priority", 7.0).put("score", 7L).put("name", "James").build()) + .put("Sally", + new MapBuilder().put(".priority", -7.0).put("score", -7L).put("name", "Sally").build()) + .put("Fred", new MapBuilder().put("score", 0.0).put("name", "Fred").build()).build(); + ref.setValue(toSet); + final Semaphore semaphore = new Semaphore(0); + final List names = new ArrayList<>(); + ref.limitToLast(5).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + names.add(snapshot.getKey()); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore, 5); + TestHelpers.assertDeepEquals(ImmutableList.of("Sally", "James", "Andrew", "Mike", "Vikrum"), + names); + } + + @Test + public void testLimitOnNodeWithPriority() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore semaphore = new Semaphore(0); + + final Map data = MapBuilder.of("a", "blah", ".priority", "priority"); + + ref.setValue(data, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref.limitToLast(2).addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Map expected = MapBuilder.of("a", "blah"); + TestHelpers.assertDeepEquals(expected, snapshot.getValue(true)); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testLimitWithMixOfNullAndNonNullPriorities() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + Map toSet = new MapBuilder() + .put("Vikrum", + new MapBuilder().put(".priority", 1000.0).put("score", 1000L).put("name", "Vikrum") + .build()) + .put("Mike", + new MapBuilder().put(".priority", 500.0).put("score", 500L).put("name", "Mike").build()) + .put("Andrew", + new MapBuilder().put(".priority", 50.0).put("score", 50L).put("name", "Andrew").build()) + .put("James", + new MapBuilder().put(".priority", 7.0).put("score", 7L).put("name", "James").build()) + .put("Sally", + new MapBuilder().put(".priority", -7.0).put("score", -7L).put("name", "Sally").build()) + .put("Fred", new MapBuilder().put("score", 0.0).put("name", "Fred").build()).build(); + + final Semaphore semaphore = new Semaphore(0); + final List names = new ArrayList<>(); + final ChildEventListener listener = ref.limitToLast(5) + .addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + names.add(snapshot.getKey()); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + ref.setValue(toSet); + TestHelpers.waitFor(semaphore, 5); + TestHelpers.assertDeepEquals(ImmutableList.of("Sally", "James", "Andrew", "Mike", "Vikrum"), + names); + ref.limitToLast(5).removeEventListener(listener); + } + + // NOTE: skipping tests for js context argument + + @Test + public void testDeletingEntireQueryWindow() + throws InterruptedException, TestFailure, TimeoutException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final ReadFuture readFuture = ReadFuture.untilCount(ref.limitToLast(2), 3); + + // wait for null event + TestHelpers.waitForRoundtrip(ref); + + ref.setValue( + MapBuilder.of( + "a", MapBuilder.of(".value", 1, ".priority", 1), + "b", MapBuilder.of(".value", 2, ".priority", 2), + "c", MapBuilder.of(".value", 3, ".priority", 3))); + + ref.updateChildren(new MapBuilder().put("b", null).put("c", null).build()); + List events = readFuture.timedGet(); + DataSnapshot snap = events.get(1).getSnapshot(); + + Map expected = MapBuilder.of("b", 2L, "c", 3L); + Object result = snap.getValue(); + TestHelpers.assertDeepEquals(expected, result); + + // The original set is still outstanding (synchronous API), so we have a + // full cache to re-window against + snap = events.get(2).getSnapshot(); + result = snap.getValue(); + TestHelpers.assertDeepEquals(MapBuilder.of("a", 1L), result); + } + + @Test + public void testOutOfViewQueryOnAChild() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final ReadFuture parentFuture = ReadFuture.untilCountAfterNull(ref.limitToLast(1), 2); + + final List childSnaps = new ArrayList<>(); + ref.child("a").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + childSnaps.add(snapshot); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new WriteFuture(ref, MapBuilder.of("a", 1, "b", 2)).timedGet(); + assertEquals(1L, childSnaps.get(0).getValue()); + ref.updateChildren(MapBuilder.of("c", 3)); + List events = parentFuture.timedGet(); + DataSnapshot snap = events.get(0).getSnapshot(); + Object result = snap.getValue(); + + Map expected = MapBuilder.of("b", 2L); + TestHelpers.assertDeepEquals(expected, result); + + snap = events.get(1).getSnapshot(); + result = snap.getValue(); + + expected = MapBuilder.of("c", 3L); + TestHelpers.assertDeepEquals(expected, result); + assertEquals(1, childSnaps.size()); + assertEquals(1L, childSnaps.get(0).getValue()); + } + + @Test + public void testChildQueryGoingOutOfViewOfTheParent() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final ReadFuture parentFuture = ReadFuture.untilCountAfterNull(ref.limitToLast(1), 3); + final List childSnaps = new ArrayList<>(); + final ValueEventListener listener = ref.child("a").addValueEventListener( + new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + childSnaps.add(snapshot); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + new WriteFuture(ref, MapBuilder.of("a", 1)).timedGet(); + assertEquals(1L, childSnaps.get(0).getValue()); + new WriteFuture(ref.child("b"), 2).timedGet(); + assertEquals(1, childSnaps.size()); + new WriteFuture(ref.child("b"), null).timedGet(); + List events = parentFuture.timedGet(); + assertEquals(1, childSnaps.size()); + + Object result; + Map expected; + + result = events.get(0).getSnapshot().getValue(); + expected = MapBuilder.of("a", 1L); + TestHelpers.assertDeepEquals(expected, result); + + result = events.get(1).getSnapshot().getValue(); + expected = MapBuilder.of("b", 2L); + TestHelpers.assertDeepEquals(expected, result); + + result = events.get(0).getSnapshot().getValue(); + expected = MapBuilder.of("a", 1L); + TestHelpers.assertDeepEquals(expected, result); + ref.child("a").removeEventListener(listener); + } + + @Test + public void testDivergingViews() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List cSnaps = new ArrayList<>(); + final List dSnaps = new ArrayList<>(); + + ref.endAt(null, "c").limitToLast(1).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + cSnaps.add(snapshot); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.endAt(null, "d").limitToLast(1).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + dSnaps.add(snapshot); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new WriteFuture(ref, MapBuilder.of("a", 1, "b", 2, "c", 3)).timedGet(); + assertEquals(1, cSnaps.size()); + final Map cExpected = MapBuilder.of("c", 3L); + TestHelpers.assertDeepEquals(cExpected, cSnaps.get(0).getValue()); + + new WriteFuture(ref.child("d"), 4).timedGet(); + assertEquals(1, cSnaps.size()); + + assertEquals(2, dSnaps.size()); + TestHelpers.assertDeepEquals(cExpected, dSnaps.get(0).getValue()); + + TestHelpers.assertDeepEquals(MapBuilder.of("d", 4L), dSnaps.get(1).getValue()); + } + + @Test + public void testRemovingQueriedElement() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List values = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + ref.limitToLast(1).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + Long val = (Long) snapshot.getValue(); + values.add(val); + if (val == 1L) { + semaphore.release(1); + } + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.setValue(MapBuilder.of("a", 1, "b", 2)); + ref.child("b").removeValue(); + TestHelpers.waitFor(semaphore); + TestHelpers.assertDeepEquals(ImmutableList.of(2L, 1L), values); + } + + @Test + public void testStartAtLimit() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue(MapBuilder.of("a", 1, "b", 2)); + DataSnapshot snap = TestHelpers.getSnap(ref.limitToFirst(1)); + + assertEquals(1L, snap.child("a").getValue()); + } + + @Test + public void testStartAtLimitWhenChildIsRemoved() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue(MapBuilder.of("a", 1, "b", 2)); + final List values = new ArrayList<>(); + final Semaphore semaphore = new Semaphore(0); + ref.limitToFirst(1).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + Long val = (Long) snapshot.getValue(); + values.add(val); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + // Wait for first value + TestHelpers.waitFor(semaphore); + assertEquals((Long) 1L, values.get(0)); + ref.child("a").removeValue(); + TestHelpers.waitFor(semaphore); + assertEquals((Long) 2L, values.get(1)); + } + + @Test + public void testStartAtWithTwoArguments() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, + MapBuilder.of( + "Walker", MapBuilder.of("name", "Walker", "score", 20, ".priority", 20), + "Michael", MapBuilder.of("name", "Michael", "score", 100, ".priority", 100))) + .timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(ref.startAt(20, "Walker").limitToFirst(2)); + List expected = ImmutableList.of("Walker", "Michael"); + int i = 0; + for (DataSnapshot child : snap.getChildren()) { + assertEquals(expected.get(i), child.getKey()); + i++; + } + assertEquals(2, i); + } + + @Test + public void testMultipleQueriesOnTheSameNode() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4) + .put("e", 5).put("f", 6).build()).timedGet(); + + final AtomicInteger limit2Called = new AtomicInteger(0); + final Semaphore semaphore = new Semaphore(0); + ref.limitToLast(2).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // Should only be called once + if (limit2Called.incrementAndGet() == 1) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + // Skipping nested calls, no re-entrant APIs in Java + + TestHelpers.waitFor(semaphore); + assertEquals(1, limit2Called.get()); + + DataSnapshot snap = TestHelpers.getSnap(ref.limitToLast(1)); + TestHelpers.assertDeepEquals(MapBuilder.of("f", 6L), snap.getValue()); + } + + @Test + public void testNodeWithDefaultListener() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4) + .put("e", 5).put("f", 6).build()).timedGet(); + + final AtomicInteger onCalled = new AtomicInteger(0); + final Semaphore semaphore = new Semaphore(0); + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // Should only be called once + if (onCalled.incrementAndGet() == 1) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore); + assertEquals(1, onCalled.get()); + + DataSnapshot snap = TestHelpers.getSnap(ref.limitToLast(1)); + TestHelpers.assertDeepEquals(MapBuilder.of("f", 6L), snap.getValue()); + } + + @Test + public void testNodeWithDefaultListenerAndNonCompleteLimit() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, MapBuilder.of("a", 1, "b", 2, "c", 3)).timedGet(); + + final AtomicInteger onCalled = new AtomicInteger(0); + final Semaphore semaphore = new Semaphore(0); + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // Should only be called once + if (onCalled.incrementAndGet() == 1) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore); + assertEquals(1, onCalled.get()); + + DataSnapshot snap = TestHelpers.getSnap(ref.limitToLast(5)); + TestHelpers.assertDeepEquals(MapBuilder.of("a", 1L, "b", 2L, "c", 3L), snap.getValue()); + } + + @Test + public void testRemoteRemoveEvent() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + Map expected = new MapBuilder() + .put("a", "a").put("b", "b").put("c", "c").put("d", "d").put("e", "e").build(); + + new WriteFuture(writer, expected).timedGet(); + + List events = new ReadFuture(reader.limitToLast(5), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + writer.child("c").removeValue(); + } + return events.size() == 2; + } + }).timedGet(); + + TestHelpers.assertDeepEquals(expected, events.get(0).getSnapshot().getValue()); + TestHelpers.assertDeepEquals( + new MapBuilder().put("a", "a").put("b", "b").put("d", "d").put("e", "e").build(), + events.get(1).getSnapshot().getValue()); + } + + @Test + public void testEndAtWithTwoArgumentsAndLimit() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + Map toSet = new MapBuilder().put("a", "a") + .put("b", "b").put("c", "c").put("d", "d").put("e", "e").put("f", "f") + .put("g", "g").put("h", "h").build(); + + new WriteFuture(ref, toSet).timedGet(); + + DataSnapshot snap = TestHelpers.getSnap(ref.endAt(null, "f").limitToLast(5)); + Map expected = new MapBuilder().put("b", "b") + .put("c", "c").put("d", "d").put("e", "e").put("f", "f").build(); + + TestHelpers.assertDeepEquals(expected, snap.getValue()); + } + + @Test + public void testComplexUpdateQueryRoot() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + Map toSet = new MapBuilder().put("a", 1).put("b", 2).put("c", 3).put("d", 4) + .put("e", 5).build(); + + new WriteFuture(writer, toSet).timedGet(); + final Semaphore semaphore = new Semaphore(0); + ReadFuture future = new ReadFuture(reader.limitToFirst(4), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + semaphore.release(1); + } + return events.size() == 2; + } + }); + + TestHelpers.waitFor(semaphore); + Map update = new MapBuilder().put("b", null).put("c", "a").put("cc", "new") + .put("cd", "new2").put("d", "gone").build(); + writer.updateChildren(update); + List events = future.timedGet(); + + Map expected = new MapBuilder().put("a", 1L).put("b", 2L).put("c", 3L) + .put("d", 4L).build(); + Object result = events.get(0).getSnapshot().getValue(); + TestHelpers.assertDeepEquals(expected, result); + + expected = new MapBuilder().put("a", 1L).put("c", "a").put("cc", "new") + .put("cd", "new2").build(); + result = events.get(1).getSnapshot().getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testUpdateQueryRoot() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + Map toSet = MapBuilder.of("bar", "a", "baz", "b", "bam", "c"); + + new WriteFuture(writer, toSet).timedGet(); + final Semaphore semaphore = new Semaphore(0); + ReadFuture future = new ReadFuture(reader.limitToLast(10), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + semaphore.release(1); + } + return events.size() == 2; + } + }); + + TestHelpers.waitFor(semaphore); + Map update = new MapBuilder().put("bar", "d").put("bam", null).put("bat", "e") + .build(); + writer.updateChildren(update); + List events = future.timedGet(); + + Map expected = MapBuilder.of("bar", "a", "baz", "b", "bam", + "c"); + Object result = events.get(0).getSnapshot().getValue(); + TestHelpers.assertDeepEquals(expected, result); + + expected = MapBuilder.of("bar", "d", "baz", "b", "bat", "e"); + result = events.get(1).getSnapshot().getValue(); + TestHelpers.assertDeepEquals(expected, result); + } + + @Test + public void testChildAddedWithLimit() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + writer.child("a").setValue(1); + writer.child("b").setValue("b"); + final Map deepObject = MapBuilder.of( + "deep", "path", + "of", MapBuilder.of("stuff", true)); + new WriteFuture(writer.child("c"), deepObject).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + reader.limitToLast(3).addChildEventListener(new ChildEventListener() { + int count = 0; + + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + if (count == 0) { + assertEquals("a", snapshot.getKey()); + assertEquals(1L, snapshot.getValue()); + } else if (count == 1) { + assertEquals("b", snapshot.getKey()); + assertEquals("b", snapshot.getValue()); + } else if (count == 2) { + assertEquals("c", snapshot.getKey()); + TestHelpers.assertDeepEquals(deepObject, snapshot.getValue()); + } else { + fail("Too many events"); + } + count++; + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore, 3); + } + + @Test + public void testChildChangedWithLimit() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + new WriteFuture(writer, MapBuilder.of("a", "something", "b", "we'll", "c", "overwrite")) + .timedGet(); + final Map deepObject = MapBuilder.of( + "deep", "path", + "of", MapBuilder.of("stuff", true)); + + final Semaphore semaphore = new Semaphore(0); + final AtomicBoolean loaded = new AtomicBoolean(false); + reader.limitToLast(3).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (loaded.compareAndSet(false, true)) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + // Wait for the read to be initialized + TestHelpers.waitFor(semaphore); + + reader.limitToLast(3).addChildEventListener(new ChildEventListener() { + int count = 0; + + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + if (count == 0) { + assertEquals("a", snapshot.getKey()); + assertEquals(1L, snapshot.getValue()); + } else if (count == 1) { + assertEquals("b", snapshot.getKey()); + assertEquals("b", snapshot.getValue()); + } else if (count == 2) { + assertEquals("c", snapshot.getKey()); + TestHelpers.assertDeepEquals(deepObject, snapshot.getValue()); + } else { + fail("Too many events"); + } + count++; + semaphore.release(1); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + writer.child("a").setValue(1); + writer.child("b").setValue("b"); + writer.child("c").setValue(deepObject); + + TestHelpers.waitFor(semaphore, 3); + } + + @Test + public void testChildRemovedWithLimit() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + writer.child("a").setValue(1); + writer.child("b").setValue("b"); + final Map deepObject = MapBuilder.of( + "deep", "path", + "of", MapBuilder.of("stuff", true)); + new WriteFuture(writer.child("c"), deepObject).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + final AtomicBoolean loaded = new AtomicBoolean(false); + reader.limitToLast(3).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (loaded.compareAndSet(false, true)) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + // Wait for the read to be initialized + TestHelpers.waitFor(semaphore); + + reader.limitToLast(3).addChildEventListener(new ChildEventListener() { + int count = 0; + + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + if (count == 0) { + assertEquals("a", snapshot.getKey()); + } else if (count == 1) { + assertEquals("b", snapshot.getKey()); + } else if (count == 2) { + assertEquals("c", snapshot.getKey()); + } else { + fail("Too many events"); + } + count++; + semaphore.release(1); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + writer.child("a").removeValue(); + writer.child("b").removeValue(); + writer.child("c").removeValue(); + + TestHelpers.waitFor(semaphore, 3); + } + + @Test + public void testChildRemovedWhenParentRemoved() + throws InterruptedException, TestFailure, ExecutionException, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + writer.child("a").setValue(1); + writer.child("b").setValue("b"); + final Map deepObject = MapBuilder.of( + "deep", "path", + "of", MapBuilder.of("stuff", true)); + new WriteFuture(writer.child("c"), deepObject).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + final AtomicBoolean loaded = new AtomicBoolean(false); + reader.limitToLast(3).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (loaded.compareAndSet(false, true)) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + // Wait for the read to be initialized + TestHelpers.waitFor(semaphore); + + reader.limitToLast(3).addChildEventListener(new ChildEventListener() { + int count = 0; + + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + if (count == 0) { + assertEquals("a", snapshot.getKey()); + } else if (count == 1) { + assertEquals("b", snapshot.getKey()); + } else if (count == 2) { + assertEquals("c", snapshot.getKey()); + } else { + fail("Too many events"); + } + count++; + semaphore.release(1); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + writer.removeValue(); + + TestHelpers.waitFor(semaphore, 3); + } + + @Test + public void testChildRemovedWhenParentSetToScalar() + throws InterruptedException, TestFailure, ExecutionException, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + writer.child("a").setValue(1); + writer.child("b").setValue("b"); + final Map deepObject = MapBuilder.of( + "deep", "path", + "of", MapBuilder.of("stuff", true)); + new WriteFuture(writer.child("c"), deepObject).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + final AtomicBoolean loaded = new AtomicBoolean(false); + reader.limitToLast(3).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (loaded.compareAndSet(false, true)) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + // Wait for the read to be initialized + TestHelpers.waitFor(semaphore); + + reader.limitToLast(3).addChildEventListener(new ChildEventListener() { + int count = 0; + + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + if (count == 0) { + assertEquals("a", snapshot.getKey()); + } else if (count == 1) { + assertEquals("b", snapshot.getKey()); + } else if (count == 2) { + assertEquals("c", snapshot.getKey()); + } else { + fail("Too many events"); + } + count++; + semaphore.release(1); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + writer.setValue("scalar"); + + TestHelpers.waitFor(semaphore, 3); + } + + @Test + public void testMultipleQueries() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + Map toSet = new MapBuilder().put("a", 1).put("b", 2).put("c", 3) + .put("d", 4).build(); + new WriteFuture(writer, toSet).timedGet(); + + TestHelpers.getSnap(reader); + + final Semaphore semaphore = new Semaphore(0); + final AtomicInteger queryAddedCount = new AtomicInteger(0); + reader.startAt(null, "d").addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + queryAddedCount.incrementAndGet(); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + final AtomicInteger defaultAddedCount = new AtomicInteger(0); + reader.addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + defaultAddedCount.incrementAndGet(); + semaphore.release(1); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore, 5); + assertEquals(1, queryAddedCount.get()); + assertEquals(4, defaultAddedCount.get()); + } + + @Test + public void testStartAtEndAtQueriesWithPriorityChanges() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final List addedFirst = new ArrayList<>(); + final List removedFirst = new ArrayList<>(); + final List addedSecond = new ArrayList<>(); + final List removedSecond = new ArrayList<>(); + + ref.startAt(0).endAt(10).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + addedFirst.add(snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + removedFirst.add(snapshot.getKey()); + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.startAt(10).endAt(20).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + addedSecond.add(snapshot.getKey()); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + // No-op + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + // No-op + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + removedSecond.add(snapshot.getKey()); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.child("a").setValue("a", 5); + ref.child("a").setValue("a", 15); + ref.child("a").setValue("a", 10); + new WriteFuture(ref.child("a"), "a", 5).timedGet(); + + assertEquals(2, addedFirst.size()); + assertEquals("a", addedFirst.get(0)); + assertEquals("a", addedFirst.get(1)); + + assertEquals(1, removedFirst.size()); + assertEquals("a", removedFirst.get(0)); + + assertEquals(1, addedSecond.size()); + assertEquals("a", addedSecond.get(0)); + + assertEquals(1, removedSecond.size()); + assertEquals("a", removedSecond.get(0)); + } + + @Test + public void testDivergingQueries() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final Map toSet = MapBuilder.of( + "a", MapBuilder.of("b", 1L, "c", 2L), + "e", 3L); + + new WriteFuture(writer, toSet).timedGet(); + + final AtomicBoolean childCalled = new AtomicBoolean(false); + final AtomicLong childValue = new AtomicLong(0); + reader.child("a/b").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + childCalled.compareAndSet(false, true); + childValue.compareAndSet(0L, snapshot.getValue(Long.class)); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + new ReadFuture(reader.limitToLast(2), new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + TestHelpers.assertDeepEquals(toSet, events.get(0).getSnapshot().getValue()); + try { + writer.child("d").setValue(4); + } catch (DatabaseException e) { // ignore + fail("Should not fail"); + } + return false; + } else { + Map expected = MapBuilder.of("d", 4L, "e", 3L); + TestHelpers.assertDeepEquals(expected, events.get(1).getSnapshot().getValue()); + return true; + } + } + }).timedGet(); + assertTrue(childCalled.get()); + assertEquals(1L, childValue.longValue()); + } + + @Test + public void testCacheInvalidation() + throws InterruptedException, TestFailure, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference reader = refs.get(0); + DatabaseReference writer = refs.get(1); + + final AtomicBoolean startChecking = new AtomicBoolean(false); + final Semaphore ready = new Semaphore(0); + final ReadFuture future = new ReadFuture(reader.limitToLast(2), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + DataSnapshot snap = events.get(events.size() - 1).getSnapshot(); + Object result = snap.getValue(); + if (startChecking.compareAndSet(false, true) && result == null) { + ready.release(1); + return false; + } + // We already initialized the location, and now the remove has + // happened + // so that + // we have no more data + return startChecking.get() && result == null; + } + }); + + TestHelpers.waitFor(ready); + for (int i = 0; i < 4; ++i) { + writer.child("k" + i).setValue(i); + } + + writer.removeValue(); + future.timedGet(); + } + + @Test + public void testIntegerKeys1() + throws InterruptedException, TestFailure, TimeoutException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore done = new Semaphore(0); + ref.setValue( + new MapBuilder().put("1", true).put("50", true).put("550", true).put("6", true) + .put("600", true).put("70", true).put("8", true).put("80", true).build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref.startAt(null, "80").addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Map expected = MapBuilder.of( + "80", true, "550", true, "600", true); + TestHelpers.assertDeepEquals(expected, snapshot.getValue()); + done.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testIntegerKeys2() + throws InterruptedException, TestFailure, TimeoutException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore done = new Semaphore(0); + ref.setValue( + new MapBuilder().put("1", true).put("50", true).put("550", true).put("6", true) + .put("600", true).put("70", true).put("8", true).put("80", true).build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref.endAt(null, "50").addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Map expected = new MapBuilder().put("1", true) + .put("6", true).put("8", true).put("50", true).build(); + TestHelpers.assertDeepEquals(expected, snapshot.getValue()); + done.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testIntegerKeys3() + throws InterruptedException, TestFailure, TimeoutException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore done = new Semaphore(0); + ref.setValue( + new MapBuilder().put("1", true).put("50", true).put("550", true).put("6", true) + .put("600", true).put("70", true).put("8", true).put("80", true).build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref.startAt(null, "50").endAt(null, "80") + .addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Map expected = MapBuilder.of( + "50", true, "70", true, "80", true); + TestHelpers.assertDeepEquals(expected, snapshot.getValue()); + done.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testMoveOutsideOfWindowIntoWindow() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + Map initialValue = MapBuilder.of( + "a", MapBuilder.of(".priority", 1L, ".value", "a"), + "b", MapBuilder.of(".priority", 2L, ".value", "b"), + "c", MapBuilder.of(".priority", 3L, ".value", "c")); + new WriteFuture(ref, initialValue).timedGet(); + final Query query = ref.limitToLast(2); + final Semaphore ready = new Semaphore(0); + final AtomicBoolean loaded = new AtomicBoolean(false); + + query.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if (loaded.compareAndSet(false, true)) { + assertEquals(2, snapshot.getChildrenCount()); + assertTrue(snapshot.hasChild("b")); + assertTrue(snapshot.hasChild("c")); + ready.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(ready); + + ref.child("a").setPriority(4L, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ready.release(1); + } + }); + + TestHelpers.waitFor(ready); + } + + @Test + public void testEmptyLimitWithBadHash() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.getRepo().setHijackHash(true); + + DataSnapshot snap = TestHelpers.getSnap(ref.limitToLast(1)); + assertNull(snap.getValue()); + + ref.getRepo().setHijackHash(false); + } + + @Test + public void testAddingQueries() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref.child("0"), "test1").timedGet(); + + ChildEventListener childEventListener = new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot dataSnapshot, String s) { + } + + @Override + public void onChildChanged(DataSnapshot dataSnapshot, String s) { + } + + @Override + public void onChildRemoved(DataSnapshot dataSnapshot) { + } + + @Override + public void onChildMoved(DataSnapshot dataSnapshot, String s) { + } + + @Override + public void onCancelled(DatabaseError firebaseError) { + } + }; + ref.startAt("a").endAt("b").addChildEventListener(childEventListener); + + DataSnapshot snapshot = new ReadFuture(ref).timedGet().get(0).getSnapshot(); + assertEquals(ImmutableList.of("test1"), snapshot.getValue()); + } + + @Test + public void testEqualToQuery() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ValueExpectationHelper expectations = new ValueExpectationHelper(); + expectations.add(ref.equalTo(1), MapBuilder.of("a", "vala")); + expectations.add(ref.equalTo(2), MapBuilder.of("b", "valb")); + expectations.add(ref.equalTo("abc"), MapBuilder.of("z", "valz")); + expectations.add(ref.equalTo(2, "no_key"), null); + expectations.add(ref.equalTo(2, "b"), MapBuilder.of("b", "valb")); + + ref.child("a").setValue("vala", 1); + ref.child("b").setValue("valb", 2); + ref.child("c").setValue("valc", 3); + ref.child("z").setValue("valz", "abc"); + + expectations.waitForEvents(); + } + + @Test + public void testRemoveListenerOnDefaultQuery() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + new WriteFuture(ref.child("a"), "foo", 100).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + final DataSnapshot[] snapshotHolder = new DataSnapshot[1]; + + ValueEventListener listener = ref.startAt(99).addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + snapshotHolder[0] = snapshot; + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Unexpected error: " + error); + } + }); + + TestHelpers.waitFor(semaphore); + Map expected = MapBuilder.of("a", "foo"); + TestHelpers.assertDeepEquals(expected, snapshotHolder[0].getValue()); + + ref.removeEventListener(listener); + + new WriteFuture(ref.child("a"), "bar", 100).timedGet(); + // the listener is removed the value should have not changed + TestHelpers.assertDeepEquals(expected, snapshotHolder[0].getValue()); + } + + @Test + public void testFallbackForOrderBy() throws InterruptedException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore done = new Semaphore(0); + + Map initial = MapBuilder.of( + "a", MapBuilder.of("foo", 3), + "b", MapBuilder.of("foo", 1), + "c", MapBuilder.of("foo", 2)); + + ref.setValue(initial, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + done.release(); + } + }); + + TestHelpers.waitFor(done); + + final List children = new ArrayList<>(); + ref.orderByChild("foo").addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + children.add(snapshot.getKey()); + if (children.size() == 3) { + done.release(); + } + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(done); + TestHelpers.assertDeepEquals(ImmutableList.of("b", "c", "a"), children); + } + + @Test + public void testSnapshotChildrenOrdering() + throws ExecutionException, TimeoutException, TestFailure, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final Semaphore semaphore = new Semaphore(0); + + final Map list = MapBuilder.of( + "a", MapBuilder.of( + "thisvaluefirst", MapBuilder.of(".value", true, ".priority", 1), + "name", MapBuilder.of(".value", "Michael", ".priority", 2), + "thisvaluelast", MapBuilder.of(".value", true, ".priority", 3)), + "b", MapBuilder.of( + "thisvaluefirst", new MapBuilder().put(".value", true).put(".priority", null).build(), + "name", MapBuilder.of(".value", "Rob", ".priority", 2), + "thisvaluelast", MapBuilder.of(".value", true, ".priority", 3)), + "c", MapBuilder.of( + "thisvaluefirst", MapBuilder.of(".value", true, ".priority", 1), + "name", MapBuilder.of(".value", "Jonny", ".priority", 2), + "thisvaluelast", MapBuilder.of(".value", true, ".priority", "somestring"))); + + new WriteFuture(writer, list).timedGet(); + + reader.orderByChild("name").addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + List expectedKeys = ImmutableList.of("thisvaluefirst", "name", "thisvaluelast"); + + // Validate that snap.child() resets order to default for child + // snaps + List orderedKeys = new ArrayList<>(); + for (DataSnapshot childSnap : snapshot.child("b").getChildren()) { + orderedKeys.add(childSnap.getKey()); + } + assertEquals(expectedKeys, orderedKeys); + + // Validate that snap.forEach() resets ordering to default for + // child snaps + List orderedNames = new ArrayList<>(); + for (DataSnapshot childSnap : snapshot.getChildren()) { + orderedNames.add(childSnap.child("name").getValue()); + orderedKeys.clear(); + for (DataSnapshot grandchildSnap : childSnap.getChildren()) { + orderedKeys.add(grandchildSnap.getKey()); + } + assertEquals(expectedKeys, orderedKeys); + } + + assertEquals(ImmutableList.of("Jonny", "Michael", "Rob"), orderedNames); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testAddingListensForTheSamePathDoesNotCheckFail() throws Throwable { + // This bug manifests itself if there's a hierarchy of query listener, + // default listener and + // one-time listener underneath. During one-time listener registration, + // sync-tree traversal + // stopped as soon as it found a complete server cache (this is the case + // for not indexed query + // view). The problem is that the same traversal was looking for a + // ancestor default view, and + // the early exit prevented from finding the default listener above the + // one-time listener. Event + // removal code path wasn't removing the listener because it stopped as + // soon as it found the + // default view. This left the zombie one-time listener and check failed + // on the second attempt + // to create a listener for the same path (asana#61028598952586). + + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore semaphore = new Semaphore(0); + + ValueEventListener dummyListen = new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }; + + ref.child("child").setValue(TestHelpers.fromJsonString("{\"name\": \"John\"}")); + + ref.orderByChild("name").equalTo("John").addValueEventListener(dummyListen); + ref.child("child").addValueEventListener(dummyListen); + TestHelpers.waitFor(semaphore, 2); + + ref.child("child").child("favoriteToy").addListenerForSingleValueEvent(dummyListen); + TestHelpers.waitFor(semaphore, 1); + + ref.child("child").child("favoriteToy").addListenerForSingleValueEvent(dummyListen); + TestHelpers.waitFor(semaphore, 1); + + ref.removeEventListener(dummyListen); + ref.child("child").removeEventListener(dummyListen); + } +} diff --git a/src/test/java/com/google/firebase/database/integration/RealtimeTestIT.java b/src/test/java/com/google/firebase/database/integration/RealtimeTestIT.java new file mode 100644 index 000000000..f6556fe0e --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/RealtimeTestIT.java @@ -0,0 +1,853 @@ +package com.google.firebase.database.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.ServerValue; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.core.DatabaseConfig; +import com.google.firebase.database.core.RepoManager; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.database.utilities.ParsedUrl; +import com.google.firebase.database.utilities.Utilities; +import com.google.firebase.testing.IntegrationTestUtils; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +public class RealtimeTestIT { + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() { + masterApp = IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() throws IOException { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testUrlParsing() { + ParsedUrl parsed = Utilities.parseUrl("https://admin-java-sdk.firebaseio.com"); + assertEquals("/", parsed.path.toString()); + assertEquals("admin-java-sdk.firebaseio.com", parsed.repoInfo.host); + assertEquals("admin-java-sdk.firebaseio.com", parsed.repoInfo.internalHost); + assertEquals(true, parsed.repoInfo.secure); + + parsed = Utilities.parseUrl("https://admin-java-sdk.firebaseio.com/foo/bar"); + assertEquals("/foo/bar", parsed.path.toString()); + assertEquals("admin-java-sdk.firebaseio.com", parsed.repoInfo.host); + assertEquals("admin-java-sdk.firebaseio.com", parsed.repoInfo.internalHost); + assertEquals(true, parsed.repoInfo.secure); + } + + @Test + public void testOnDisconnectSetWorks() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer.child("disconnected"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + Object snap = events.get(events.size() - 1).getSnapshot().getValue(); + return snap != null; + } + }); + + final ReadFuture readerFuture = new ReadFuture(reader.child("disconnected"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + Object snap = events.get(events.size() - 1).getSnapshot().getValue(); + return snap != null; + } + }); + + // Wait for initial (null) value on both reader and writer. + TestHelpers.waitFor(valSemaphore, 2); + + Object expected = "dummy"; + writer.child("disconnected").onDisconnect().setValue(expected, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + RepoManager.interrupt(ctx); + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord writerEventRecord = writerFuture.timedGet().get(1); + EventRecord readerEventRecord = readerFuture.timedGet().get(1); + + RepoManager.resume(ctx); + + TestHelpers.assertDeepEquals(expected, writerEventRecord.getSnapshot().getValue()); + TestHelpers.assertDeepEquals(expected, readerEventRecord.getSnapshot().getValue()); + } + + @Test + public void testOnDisconnectSetWithPriorityWorks() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer.child("disconnected"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + Object snap = events.get(events.size() - 1).getSnapshot().getValue(); + valSemaphore.release(); + return snap != null; + } + }); + + final ReadFuture readerFuture = new ReadFuture(reader.child("disconnected"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + Object snap = events.get(events.size() - 1).getSnapshot().getValue(); + valSemaphore.release(); + return snap != null; + } + }); + + // Wait for initial (null) value on both reader and writer. + TestHelpers.waitFor(valSemaphore, 2); + + String expectedPriority = "12345"; + writer.child("disconnected").onDisconnect().setValue(true, expectedPriority, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + RepoManager.interrupt(ctx); + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord writerEventRecord = writerFuture.timedGet().get(1); + final EventRecord readerEventRecord = readerFuture.timedGet().get(1); + + RepoManager.resume(ctx); + + TestHelpers.assertDeepEquals(true, writerEventRecord.getSnapshot().getValue()); + TestHelpers.assertDeepEquals(expectedPriority, writerEventRecord.getSnapshot().getPriority()); + TestHelpers.assertDeepEquals(true, readerEventRecord.getSnapshot().getValue()); + TestHelpers.assertDeepEquals(expectedPriority, readerEventRecord.getSnapshot().getPriority()); + } + + @Test + public void testOnDisconnectRemoveWorks() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer.child("foo"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 3; + } + }); + + final ReadFuture readerFuture = new ReadFuture(reader.child("foo"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 3; + } + }); + + // Wait for initial (null) value on both reader and writer. + TestHelpers.waitFor(valSemaphore, 2); + + writer.child("foo").setValue("bar", new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo").onDisconnect().removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + RepoManager.interrupt(ctx); + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord writerEventRecord = writerFuture.timedGet().get(2); + EventRecord readerEventRecord = readerFuture.timedGet().get(2); + + RepoManager.resume(ctx); + + TestHelpers.assertDeepEquals(null, writerEventRecord.getSnapshot().getValue()); + TestHelpers.assertDeepEquals(null, readerEventRecord.getSnapshot().getValue()); + } + + @Test + public void testOnDisconnectUpdateWorks() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer.child("foo"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 4; + } + }); + + final ReadFuture readerFuture = new ReadFuture(reader.child("foo"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 4; + } + }); + + // Wait for initial (null) value on both reader and writer. + TestHelpers.waitFor(valSemaphore, 2); + + Map initialValues = new MapBuilder().put("bar", "a").put("baz", "b").build(); + writer.child("foo").setValue(initialValues, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + Map updatedValues = new MapBuilder().put("baz", "c").put("bat", "d").build(); + writer.child("foo").onDisconnect().updateChildren(updatedValues, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + RepoManager.interrupt(ctx); + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord writerEventRecord = writerFuture.timedGet().get(3); + EventRecord readerEventRecord = readerFuture.timedGet().get(3); + + RepoManager.resume(ctx); + + Map expected = new MapBuilder().put("bar", "a").put("baz", "c").put("bat", "d") + .build(); + + TestHelpers.assertDeepEquals(expected, writerEventRecord.getSnapshot().getValue()); + TestHelpers.assertDeepEquals(expected, readerEventRecord.getSnapshot().getValue()); + } + + @Test + public void testOnDisconnectTriggersSingleLocalValueEventForWriter() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 1); + DatabaseReference writer = refs.get(0); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final AtomicInteger callbackCount = new AtomicInteger(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + callbackCount.incrementAndGet(); + valSemaphore.release(1); + return events.size() == 2; + } + }); + TestHelpers.waitFor(valSemaphore); + + final Semaphore opSemaphore = new Semaphore(0); + writer.child("foo").onDisconnect().setValue( + new MapBuilder().put("bar", "a").put("baz", "b").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo").onDisconnect().updateChildren(new MapBuilder().put("bam", "c").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo/baz").onDisconnect().removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + RepoManager.interrupt(ctx); + + TestHelpers.waitFor(valSemaphore); + EventRecord writerEventRecord = writerFuture.timedGet().get(1); + + RepoManager.resume(ctx); + + Map expected = new MapBuilder() + .put("foo", new MapBuilder().put("bam", "c").put("bar", "a").build()).build(); + TestHelpers.assertDeepEquals(expected, writerEventRecord.getSnapshot().getValue()); + assertTrue(callbackCount.get() == 2); + } + + @Test + public void testOnDisconnectTriggersSingleLocalValueEventForReader() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final AtomicInteger callbackCount = new AtomicInteger(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + callbackCount.incrementAndGet(); + valSemaphore.release(1); + return events.size() == 2; + } + }); + TestHelpers.waitFor(valSemaphore); + + final Semaphore opSemaphore = new Semaphore(0); + writer.child("foo").onDisconnect().setValue( + new MapBuilder().put("bar", "a").put("baz", "b").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo").onDisconnect().updateChildren(new MapBuilder().put("bam", "c").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo/baz").onDisconnect().removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + RepoManager.interrupt(ctx); + + TestHelpers.waitFor(valSemaphore); + EventRecord readerEventRecord = readerFuture.timedGet().get(1); + + RepoManager.resume(ctx); + + Map expected = new MapBuilder() + .put("foo", new MapBuilder().put("bam", "c").put("bar", "a").build()).build(); + TestHelpers.assertDeepEquals(expected, readerEventRecord.getSnapshot().getValue()); + assertTrue(callbackCount.get() == 2); + } + + @Test + public void testOnDisconnectTriggersSingleLocalValueEventForWriterWithQuery() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 1); + DatabaseReference writer = refs.get(0); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final AtomicInteger callbackCount = new AtomicInteger(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + callbackCount.incrementAndGet(); + valSemaphore.release(1); + return events.size() == 2; + } + }); + TestHelpers.waitFor(valSemaphore); + + final Semaphore opSemaphore = new Semaphore(0); + writer.child("foo").onDisconnect().setValue( + new MapBuilder().put("bar", "a").put("baz", "b").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo").onDisconnect().updateChildren(new MapBuilder().put("bam", "c").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo/baz").onDisconnect().removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + RepoManager.interrupt(ctx); + + TestHelpers.waitFor(valSemaphore); + EventRecord writerEventRecord = writerFuture.timedGet().get(1); + + RepoManager.resume(ctx); + + Map expected = new MapBuilder() + .put("foo", new MapBuilder().put("bam", "c").put("bar", "a").build()).build(); + TestHelpers.assertDeepEquals(expected, writerEventRecord.getSnapshot().getValue()); + assertTrue(callbackCount.get() == 2); + } + + @Test + public void testOnDisconnectTriggersSingleLocalValueEventForReaderWithQuery() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + + final AtomicInteger callbackCount = new AtomicInteger(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + callbackCount.incrementAndGet(); + valSemaphore.release(1); + return events.size() == 2; + } + }); + TestHelpers.waitFor(valSemaphore); + + final Semaphore opSemaphore = new Semaphore(0); + writer.child("foo").onDisconnect().setValue( + new MapBuilder().put("bar", "a").put("baz", "b").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo").onDisconnect().updateChildren(new MapBuilder().put("bam", "c").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo/baz").onDisconnect().removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + RepoManager.interrupt(ctx); + + TestHelpers.waitFor(valSemaphore); + EventRecord readerEventRecord = readerFuture.timedGet().get(1); + + RepoManager.resume(ctx); + + Map expected = new MapBuilder() + .put("foo", new MapBuilder().put("bam", "c").put("bar", "a").build()).build(); + TestHelpers.assertDeepEquals(expected, readerEventRecord.getSnapshot().getValue()); + assertTrue(callbackCount.get() == 2); + } + + @Test + public void testOnDisconnectDeepMergeTriggersOnlyOneValueEventForReaderWithQuery() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + + final AtomicInteger callbackCount = new AtomicInteger(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture readerFuture = new ReadFuture(reader, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + callbackCount.incrementAndGet(); + valSemaphore.release(1); + return events.size() == 4; + } + }); + TestHelpers.waitFor(valSemaphore); + + final Semaphore opSemaphore = new Semaphore(0); + Map initialValues = new MapBuilder().put("a", 1) + .put("b", new MapBuilder().put("c", true).put("d", "scalar") + .put("e", new MapBuilder().put("f", "hooray").build()).build()) + .build(); + writer.setValue(initialValues, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(valSemaphore); + TestHelpers.waitFor(opSemaphore); + + writer.child("b/c").onDisconnect().setValue(false, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("b/d").onDisconnect().removeValue(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(1); + } + }); + TestHelpers.waitFor(opSemaphore); + + RepoManager.interrupt(ctx); + + TestHelpers.waitFor(valSemaphore); + EventRecord readerEventRecord = readerFuture.timedGet().get(3); + + RepoManager.resume(ctx); + + Map expected = new MapBuilder().put("a", 1L).put("b", new MapBuilder() + .put("c", false).put("e", new MapBuilder().put("f", "hooray").build()).build()).build(); + TestHelpers.assertDeepEquals(expected, readerEventRecord.getSnapshot().getValue()); + assertEquals(4, callbackCount.get()); + } + + @Test + public void testOnDisconnectCancelWorks() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer.child("foo"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 3; + } + }); + + final ReadFuture readerFuture = new ReadFuture(reader.child("foo"), + new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + return events.size() == 3; + } + }); + + // Wait for initial (null) value on both reader and writer. + TestHelpers.waitFor(valSemaphore, 2); + + Map initialValues = new MapBuilder().put("bar", "a").put("baz", "b").build(); + writer.child("foo").setValue(initialValues, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + Map updatedValues = new MapBuilder().put("baz", "c").put("bat", "d").build(); + writer.child("foo").onDisconnect().updateChildren(updatedValues, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + writer.child("foo/bat").onDisconnect().cancel(new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + RepoManager.interrupt(ctx); + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord writerEventRecord = writerFuture.timedGet().get(2); + EventRecord readerEventRecord = readerFuture.timedGet().get(2); + + RepoManager.resume(ctx); + + Map expected = new MapBuilder().put("bar", "a").put("baz", "c").build(); + + TestHelpers.assertDeepEquals(expected, writerEventRecord.getSnapshot().getValue()); + TestHelpers.assertDeepEquals(expected, readerEventRecord.getSnapshot().getValue()); + } + + @Test + public void testOnDisconnectWithServerValuesWorks() + throws TestFailure, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 1); + DatabaseReference writer = refs.get(0); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final Semaphore opSemaphore = new Semaphore(0); + final Semaphore valSemaphore = new Semaphore(0); + final ReadFuture writerFuture = new ReadFuture(writer, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + valSemaphore.release(); + Object snapVal = events.get(events.size() - 1).getSnapshot().getValue(); + return snapVal != null; + } + }); + + // Wait for initial (null) value. + TestHelpers.waitFor(valSemaphore); + + Map initialValues = new MapBuilder() + .put("a", ServerValue.TIMESTAMP).put("b", new MapBuilder() + .put(".value", ServerValue.TIMESTAMP).put(".priority", ServerValue.TIMESTAMP).build()) + .build(); + + writer.onDisconnect().setValue(initialValues, ServerValue.TIMESTAMP, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + RepoManager.interrupt(ctx); + opSemaphore.release(); + } + }); + TestHelpers.waitFor(opSemaphore); + + EventRecord readerEventRecord = writerFuture.timedGet().get(1); + DataSnapshot snap = readerEventRecord.getSnapshot(); + + RepoManager.resume(ctx); + + assertEquals(snap.child("a").getValue().getClass(), Long.class); + assertEquals(snap.getPriority().getClass(), Double.class); + assertEquals(snap.getPriority(), snap.child("b").getPriority()); + assertEquals(snap.child("a").getValue(), snap.child("b").getValue()); + assert (Math.abs( + System.currentTimeMillis() - Long.parseLong(snap.child("a").getValue().toString())) < 2000); + } + + // TODO: Find better way to test shutdown behavior. This test is not worth a + // 13-second pause (6 + // second sleep and then 7 second timeout via timedGet())! + @Test + @Ignore + public void testShutdown() + throws InterruptedException, ExecutionException, TestFailure, TimeoutException { + DatabaseConfig config = TestHelpers.getDatabaseConfig(masterApp); + + // Shut it down right away + RepoManager.interrupt(config); + Thread.sleep(6 * 1000); // Long enough for all of the threads to exit + assertTrue(config.isStopped()); + + // Test that we can use an existing ref + DatabaseReference ref = FirebaseDatabase.getInstance().getReference(); + DatabaseReference pushed = ref.push(); + try { + new WriteFuture(pushed, "foo").timedGet(); + fail("Should time out, we're offline"); + } catch (TimeoutException t) { + // Expected, we're offline + } + assertFalse(config.isStopped()); + RepoManager.resume(config); + + final Semaphore ready = new Semaphore(0); + ref.child(".info/connected").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + Boolean connected = snapshot.getValue(Boolean.class); + if (connected) { + ready.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + // Wait for us to be connected so we send the buffered put + TestHelpers.waitFor(ready); + + DataSnapshot snap = TestHelpers.getSnap(pushed); + assertEquals("foo", snap.getValue(String.class)); + } + + @Test + public void testWritesToSameLocationWhileOfflineAreInOrder() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + DatabaseReference.goOffline(); + for (int i = 0; i < 100; i++) { + ref.setValue(i); + } + // This should be the last write and the actual value + WriteFuture future = new WriteFuture(ref, 100); + DatabaseReference.goOnline(); + future.timedGet(); + + final Semaphore semaphore = new Semaphore(0); + ref.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals(100L, snapshot.getValue()); + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Shouldn't be cancelled"); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testOnDisconnectIsNotRerunOnReconnect() + throws TestFailure, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.resume(ctx); + + final Semaphore semaphore = new Semaphore(0); + + // Will ensure that the operation is queued + RepoManager.interrupt(ctx); + + final int[] counter = new int[1]; + + ref.child("disconnected").onDisconnect().setValue(true, + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNull(error); + semaphore.release(); + counter[0]++; + } + }); + + // Will trigger sending the onDisconnect + RepoManager.resume(ctx); + + // Should be complete initially + TestHelpers.waitFor(semaphore); + // One onComplete called + assertEquals(1, counter[0]); + + // Will trigger a reconnect + RepoManager.interrupt(ctx); + RepoManager.resume(ctx); + + // Make sure we sent all outstanding onDisconnects + TestHelpers.waitForRoundtrip(ref); + // Two are needed because writes are restored first, then onDisconnects + TestHelpers.waitForRoundtrip(ref); + assertEquals(1, counter[0]); // No onComplete should have triggered + } +} diff --git a/src/test/java/com/google/firebase/database/integration/RulesTestIT.java b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java new file mode 100644 index 000000000..e423e0ce2 --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java @@ -0,0 +1,406 @@ +package com.google.firebase.database.integration; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseCredentials; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.TestTokenProvider; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.core.DatabaseConfig; +import com.google.firebase.database.core.RepoManager; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.database.util.JsonMapper; +import com.google.firebase.tasks.Tasks; +import com.google.firebase.testing.IntegrationTestUtils; +import com.google.firebase.testing.TestUtils; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class RulesTestIT { + + private static final String DEFAULT_RULES_STRING = + "{\n \"rules\": {\n \".read\": true,\n \".write\": true\n }\n}"; + + private static final Map testRules; + + static { + testRules = new MapBuilder() + .put("read_only", MapBuilder.of(".read", true)) + .put("write_only", MapBuilder.of(".write", true)) + .put("read_and_write", + new MapBuilder().put(".write", true) + .put(".read", + true) + .build()) + .put("any_auth", + new MapBuilder().put(".write", "auth != null").put(".read", "auth != null").build()) + .put("revocable", new MapBuilder().put(".write", true) + .put(".read", + "data.child('public').val() == true && data.child('hidden').val() != true") + .build()) + .put("users", new MapBuilder().put(".write", true).put(".read", true) + .put("$user", new MapBuilder().put(".validate", "newData.hasChildren(['name', 'age'])") + .put("name", + new MapBuilder().put(".validate", "newData.isString() == true").build()) + .put("age", + new MapBuilder() + .put(".validate", "newData.isNumber() && newData.val() > " + "13").build()) + .build()) + .build()) + .build(); + } + + private static DatabaseReference reader; + private static DatabaseReference writer; + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() throws IOException { + // Init app with non-admin privileges + Map auth = MapBuilder.of("uid", "my-service-worker"); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredential(FirebaseCredentials + .fromCertificate(IntegrationTestUtils.getServiceAccountCertificate())) + .setDatabaseUrl(IntegrationTestUtils.getDatabaseUrl()) + .setDatabaseAuthVariableOverride(auth) + .build(); + masterApp = FirebaseApp.initializeApp(options, "RulesTestIT"); + + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + reader = refs.get(0); + writer = refs.get(1); + String rules = JsonMapper.serializeJson( + MapBuilder.of("rules", MapBuilder.of(writer.getKey(), testRules))); + uploadRules(rules); + TestHelpers.waitForRoundtrip(writer.getRoot()); + } + + @AfterClass + public static void tearDownClass() throws IOException { + uploadRules(DEFAULT_RULES_STRING); + TestHelpers.waitForRoundtrip(writer.getRoot()); + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + private static void uploadRules(String rules) throws IOException { + IntegrationTestUtils.AppHttpClient client = new IntegrationTestUtils.AppHttpClient(masterApp); + IntegrationTestUtils.ResponseInfo response = client.put("/.settings/rules.json", rules); + assertEquals(200, response.getStatus()); + } + + @Test + public void testWriteOperationSetsErrorOnFailure() throws InterruptedException { + DatabaseReference ref = writer.child("read_only"); + + final Semaphore semaphore = new Semaphore(0); + final AtomicReference result = new AtomicReference<>(); + ref.setValue("value", new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + result.compareAndSet(null, error); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + assertNotNull(result.get()); + + // All of the other writing methods are just testing the server. We're just + // testing the + // error propagation. + } + + @Test + public void testFailedListensDoNotDisruptOtherListens() + throws TestFailure, TimeoutException, InterruptedException { + // Wait until we're connected + ReadFuture.untilEquals(reader.getRoot().child(".info/connected"), true).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + reader.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + fail("Should not get data"); + } + + @Override + public void onCancelled(DatabaseError error) { + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + final AtomicBoolean saw42 = new AtomicBoolean(false); + final ValueEventListener listener = reader.child("read_and_write") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // No-op + Integer value = snapshot.getValue(Integer.class); + if (value != null) { + if (value == 42) { + assertTrue(saw42.compareAndSet(false, true)); + semaphore.release(1); + } else if (value == 84) { + assertTrue(saw42.get()); + semaphore.release(1); + } else { + fail("unexpected value"); + } + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("This one shouldn't fail"); + } + }); + + writer.child("read_and_write").setValue(42); + TestHelpers.waitFor(semaphore); + writer.child("read_and_write").setValue(84); + TestHelpers.waitFor(semaphore); + reader.child("read_and_write").removeEventListener(listener); + } + + @Test + public void testFailedListensDoNotDisruptOtherListens2() + throws TestFailure, TimeoutException, InterruptedException { + // Wait until we're connected + ReadFuture.untilEquals(reader.getRoot().child(".info/connected"), true).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + final AtomicBoolean saw42 = new AtomicBoolean(false); + final ValueEventListener listener = reader.child("read_and_write") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // No-op + Integer value = snapshot.getValue(Integer.class); + if (value != null) { + if (value == 42) { + assertTrue(saw42.compareAndSet(false, true)); + semaphore.release(1); + } else if (value == 84) { + assertTrue(saw42.get()); + semaphore.release(1); + } else { + fail("unexpected value"); + } + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("This one shouldn't fail"); + } + }); + + writer.child("read_and_write").setValue(42); + TestHelpers.waitFor(semaphore); + reader.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + fail("Should not get data"); + } + + @Override + public void onCancelled(DatabaseError error) { + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + + writer.child("read_and_write").setValue(84); + TestHelpers.waitFor(semaphore); + reader.child("read_and_write").removeEventListener(listener); + } + + @Test + public void testListenRevocation() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + new WriteFuture(writer.child("revocable"), + new MapBuilder().put("public", true).put("data", 1).build()).timedGet(); + + final AtomicBoolean valueHit = new AtomicBoolean(false); + final AtomicInteger value = new AtomicInteger(0); + final Semaphore semaphore = new Semaphore(0); + reader.child("revocable/data").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + valueHit.compareAndSet(false, true); + value.compareAndSet(0, snapshot.getValue(Integer.class)); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + assertTrue(valueHit.get()); + assertEquals(1, value.get()); + + writer.child("revocable/public").setValue(false); + writer.child("revocable/data").setValue(2); + TestHelpers.waitFor(semaphore); + assertTrue(valueHit.get()); + + writer.child("revocable/public").setValue(true); + new WriteFuture(writer.child("revocable/data"), 3).timedGet(); + + // Ok, the listen was cancelled, create a new one now. + ValueEventListener listener = reader.child("revocable/data") + .addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + assertEquals(3, (int) snapshot.getValue(Integer.class)); + semaphore.release(1); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("This listen shouldn't fail"); + } + }); + + TestHelpers.waitFor(semaphore); + reader.child("revocable/data").removeEventListener(listener); + } + + @Test + public void testFailedSetsRolledBack() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference fredRef = reader.child("users/fred"); + + new WriteFuture(fredRef, new MapBuilder().put("name", "Fred").put("age", 19).build()) + .timedGet(); + + final Semaphore semaphore = new Semaphore(0); + ReadFuture future = new ReadFuture(fredRef.child("age"), new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + semaphore.release(1); + } + return events.size() == 3; + } + }); + + // Wait for initial data + TestHelpers.waitFor(semaphore); + + fredRef.child("age").setValue(12, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNotNull(error); + semaphore.release(1); + } + }); + + List events = future.timedGet(); + assertEquals(19, (int) events.get(0).getSnapshot().getValue(Integer.class)); + assertEquals(12, (int) events.get(1).getSnapshot().getValue(Integer.class)); + assertEquals(19, (int) events.get(2).getSnapshot().getValue(Integer.class)); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testFailedUpdatesRolledBack() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference fredRef = reader.child("users/fred"); + + new WriteFuture(fredRef, new MapBuilder().put("name", "Fred").put("age", 19).build()) + .timedGet(); + + final Semaphore semaphore = new Semaphore(0); + ReadFuture future = new ReadFuture(fredRef.child("age"), new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + if (events.size() == 1) { + semaphore.release(1); + } + return events.size() == 3; + } + }); + + // Wait for initial data + TestHelpers.waitFor(semaphore); + + Map update = MapBuilder.of("age", 12); + fredRef.updateChildren(update, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertNotNull(error); + semaphore.release(1); + } + }); + + List events = future.timedGet(); + assertEquals(19, (int) events.get(0).getSnapshot().getValue(Integer.class)); + assertEquals(12, (int) events.get(1).getSnapshot().getValue(Integer.class)); + assertEquals(19, (int) events.get(2).getSnapshot().getValue(Integer.class)); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testStillAuthenticatedAfterReconnect() + throws InterruptedException, ExecutionException, TestFailure, TimeoutException { + DatabaseConfig config = TestHelpers.getDatabaseConfig(masterApp); + DatabaseReference root = FirebaseDatabase.getInstance(masterApp).getReference(); + DatabaseReference ref = root.child(writer.getPath().toString()); + RepoManager.interrupt(config); + RepoManager.resume(config); + DatabaseError err = new WriteFuture(ref.child("any_auth"), true).timedGet(); + assertNull(err); + } + + @Test + public void testAuthenticatedImmediatelyAfterTokenChange() throws Exception { + DatabaseConfig config = TestHelpers.getDatabaseConfig(masterApp); + TestTokenProvider provider = new TestTokenProvider(TestHelpers.getExecutorService(config)); + config.setAuthTokenProvider(provider); + + DatabaseReference root = FirebaseDatabase.getInstance(masterApp).getReference(); + DatabaseReference ref = root.child(writer.getPath().toString()); + + String token = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken(masterApp, true), + TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).getToken(); + provider.setToken(token); + + DatabaseError err = new WriteFuture(ref.child("any_auth"), true).timedGet(); + assertNull(err); + } +} diff --git a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java index 7e933bccd..63d74be77 100644 --- a/src/test/java/com/google/firebase/database/integration/ShutdownExample.java +++ b/src/test/java/com/google/firebase/database/integration/ShutdownExample.java @@ -18,7 +18,7 @@ public static void main(String[] args) { FirebaseApp app = FirebaseApp.initializeApp( new FirebaseOptions.Builder() - .setDatabaseUrl("http://gsoltis.fblocal.com:9000") + .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); FirebaseDatabase db = FirebaseDatabase.getInstance(app); diff --git a/src/test/java/com/google/firebase/database/integration/TransactionTestIT.java b/src/test/java/com/google/firebase/database/integration/TransactionTestIT.java new file mode 100644 index 000000000..5552967ff --- /dev/null +++ b/src/test/java/com/google/firebase/database/integration/TransactionTestIT.java @@ -0,0 +1,1949 @@ +package com.google.firebase.database.integration; + +import static com.google.firebase.database.TestHelpers.fromSingleQuotedString; +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.assertTrue; +import static org.junit.Assert.fail; + +import com.google.common.collect.Iterables; +import com.google.firebase.FirebaseApp; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.database.ChildEventListener; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseException; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.DatabaseReference.CompletionListener; +import com.google.firebase.database.EventRecord; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.MutableData; +import com.google.firebase.database.ServerValue; +import com.google.firebase.database.TestChildEventListener; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.Transaction; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.core.AuthTokenProvider; +import com.google.firebase.database.core.DatabaseConfig; +import com.google.firebase.database.core.RepoManager; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.database.future.WriteFuture; +import com.google.firebase.testing.IntegrationTestUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +public class TransactionTestIT { + + private static FirebaseApp masterApp; + + @BeforeClass + public static void setUpClass() throws IOException { + masterApp = IntegrationTestUtils.initDefaultApp(); + } + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testNewValueIsImmediatelyVisible() throws Exception { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.child("foo").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + currentData.setValue(42); + } catch (DatabaseException e) { + fail("Should not fail"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + if (error != null || !committed) { + fail("Transaction should succeed"); + } + } + }); + + DataSnapshot snap = new ReadFuture(ref.child("foo")).timedGet().get(0).getSnapshot(); + + assertEquals(42L, snap.getValue()); + } + + @Test + public void testEventRaisedForNewValue() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + EventHelper helper = new EventHelper().addValueExpectation(ref).startListening(); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + currentData.setValue(42); + } catch (DatabaseException e) { + fail("Should not throw"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + // No-op + } + }); + + assertTrue(helper.waitForEvents()); + helper.cleanup(); + } + + @Test + public void testNonAbortedTransactionSetsCommittedToTrue() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + currentData.setValue(42); + } catch (DatabaseException e) { + fail("Should not fail"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + if (error != null || !committed) { + fail("Transaction should succeed"); + } else { + assertEquals(42L, currentData.getValue()); + semaphore.release(1); + } + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testAbortedTransactionSetsCommittedToFalse() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + return Transaction.abort(); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertFalse(committed); + assertNull(currentData.getValue()); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testSetDataReconnectDoTransactionThatAborts() throws + TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + + new WriteFuture(refs.get(0), 42).timedGet(); + + DatabaseReference node = refs.get(1); + + // Go offline to ensure our listen doesn't complete before the transaction + // runs. + node.getDatabase().goOffline(); + + final AtomicInteger count = new AtomicInteger(0); + ReadFuture readFuture = new ReadFuture(node, new ReadFuture.CompletionCondition() { + @Override + public boolean isComplete(List events) { + Object latestValue = Iterables.getLast(events).getSnapshot().getValue(); + if (events.size() == 1) { + assertEquals("temp value", latestValue); + } else if (events.size() == 2) { + assertEquals(42L, latestValue); + } else { + fail("An extra event was detected"); + } + Object val = Iterables.getLast(events).getSnapshot().getValue(); + return val != null && count.incrementAndGet() == 2; + } + }); + + node.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() == null) { + try { + currentData.setValue("temp value"); + } catch (DatabaseException e) { + fail("Exception thrown: " + e.toString()); + } + return Transaction.success(currentData); + } else { + return Transaction.abort(); + } + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertFalse(committed); + assertEquals(42L, currentData.getValue()); + } + }); + + node.getDatabase().goOnline(); + + List events = readFuture.timedGet(); + Object result = events.get(0).getSnapshot().getValue(); + assertEquals("temp value", result); + result = events.get(1).getSnapshot().getValue(); + assertEquals(42L, result); + } + + @Test + public void testTransactionCreateNode() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final AtomicInteger events = new AtomicInteger(0); + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + // ignore initial null from the server if we get it. + if (!(events.get() == 0 && snapshot.getValue() == null)) { + events.incrementAndGet(); + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + final Semaphore semaphore = new Semaphore(0); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + currentData.setValue(42); + } catch (DatabaseException e) { + fail("Should not fail"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + if (error != null || !committed) { + fail("Transaction should succeed"); + } else { + assertEquals(42L, currentData.getValue()); + semaphore.release(1); + } + } + }); + TestHelpers.waitFor(semaphore); + assertEquals(1, events.get()); + } + + @Test + public void testTransactionUpdateExistingChildNodes() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + EventHelper helper = new EventHelper().addValueExpectation(reader.child("a")) + .addValueExpectation(reader.child("b")).startListening(true); + + writer.child("a").setValue(42); + new WriteFuture(writer.child("b"), 42).timedGet(); + + assertTrue(helper.waitForEvents()); + + helper.addValueExpectation(reader.child("b")).startListening(); + + final Semaphore semaphore = new Semaphore(0); + reader.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + currentData.child("a").setValue(42); + currentData.child("b").setValue(87); + return Transaction.success(currentData); + } catch (DatabaseException e) { + fail("Should not throw"); + return Transaction.abort(); + } + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + Map expected = new MapBuilder().put("a", 42L).put("b", 87L).build(); + assertEquals(expected, currentData.getValue()); + semaphore.release(1); + } + }); + + assertTrue(helper.waitForEvents()); + TestHelpers.waitFor(semaphore); + helper.cleanup(); + } + + @Test + public void testTransactionIsOnlyCalledOnceWhenInitializingAnEmptyNode() throws + InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final AtomicInteger called = new AtomicInteger(0); + final Semaphore semaphore = new Semaphore(0); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + called.incrementAndGet(); + assertNull(currentData.getValue()); + try { + currentData.child("a").setValue(5); + currentData.child("b").setValue(6); + return Transaction.success(currentData); + } catch (DatabaseException e) { + fail("Should not throw"); + return Transaction.abort(); + } + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + assertEquals(1, called.get()); + } + + @Test + public void testSecondTransactionRunImmediatelyOnPreviousOutput() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref = refs.get(0); + + final AtomicBoolean firstRun = new AtomicBoolean(false); + final Semaphore first = new Semaphore(0); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + assertTrue(firstRun.compareAndSet(false, true)); + try { + currentData.setValue(42); + return Transaction.success(currentData); + } catch (DatabaseException e) { + fail("Should not throw"); + return Transaction.abort(); + } + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + first.release(1); + } + }); + + final AtomicBoolean secondRun = new AtomicBoolean(false); + final Semaphore second = new Semaphore(0); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + assertTrue(secondRun.compareAndSet(false, true)); + try { + currentData.setValue(84); + return Transaction.success(currentData); + } catch (DatabaseException e) { + fail("Should not throw"); + return Transaction.abort(); + } + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + second.release(1); + } + }); + TestHelpers.waitFor(first); + TestHelpers.waitFor(second); + + DataSnapshot snap = TestHelpers.getSnap(refs.get(1)); + assertEquals(84L, snap.getValue()); + } + + @Test + public void testSetCancelsPendingTransactions() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + // We do 3 transactions: 1) At /foo, 2) At /, and 3) At /bar. + // Only #1 is sent to the server immediately (since 2 depends on 1 and 3 + // depends on 2). + // We set /foo to 0. + // - Transaction #1 should complete as planned (since it was already sent). + // - Transaction #2 should be aborted by the set. We keep it from completing + // by hijacking + // the + // hash + // - Transaction #3 should be re-run after #2 is reverted, and then be sent + // to the server + // and + // succeed. + + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + final List nodeSnaps = new ArrayList<>(); + final AtomicBoolean firstDone = new AtomicBoolean(false); + final AtomicBoolean secondDone = new AtomicBoolean(false); + final AtomicInteger thirdRunCount = new AtomicInteger(0); + + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + nodeSnaps.add(snapshot); + if (nodeSnaps.size() == 1) { + // we got the initial data + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + ref.child("foo").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + TestHelpers.waitFor(semaphore); + + final AtomicBoolean firstRun = new AtomicBoolean(false); + ref.child("foo").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + assertTrue(firstRun.compareAndSet(false, true)); + currentData.setValue(42); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) { + assertTrue(committed); + assertEquals(42L, snapshot.getValue()); + firstDone.set(true); + } + }); + + final AtomicBoolean secondRun = new AtomicBoolean(false); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + ref.getRepo().setHijackHash(true); + assertTrue(secondRun.compareAndSet(false, true)); + currentData.child("foo").setValue(84); + currentData.child("bar").setValue(1); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) { + ref.getRepo().setHijackHash(false); + assertFalse(committed); + secondDone.set(true); + } + }); + + ref.child("bar").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + int count = thirdRunCount.incrementAndGet(); + if (count == 1) { + assertEquals(1L, currentData.getValue()); + currentData.setValue("first"); + return Transaction.success(currentData); + } else { + // NOTE: This may get hit more than once because the previous + // transaction + // may still be hijacking transaction hashes. + assertNull(currentData.getValue()); + currentData.setValue("second"); + return Transaction.success(currentData); + } + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) { + assertEquals(null, error); + assertTrue(committed); + assertEquals("second", snapshot.getValue()); + semaphore.release(); + } + }); + + // This rolls back the second transaction, and triggers a re-run of the + // third. + // However, a new value event won't be triggered until the listener is + // complete, + // so we're left with the last value event + ref.child("foo").setValue(0); + + TestHelpers.waitFor(semaphore); + + assertTrue(firstDone.get()); + assertTrue(secondDone.get()); + + // Note that the set actually raises two events, one overlaid on top of the + // original + // transaction + // value, and a second one with the re-run value from the third transaction + } + + @Test + public void testTransactionSet() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + assertNull(currentData.getValue()); + currentData.setValue("hi!"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + semaphore.release(1); + } + }); + + ref.setValue("foo"); + ref.setValue("bar"); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testPriorityNotPreservedWhenSettingData() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore semaphore = new Semaphore(0); + + final List snaps = new ArrayList<>(); + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + snaps.add(snapshot); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + ref.setValue("test", 5); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue("new value"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + + assertEquals(2, snaps.size()); + assertNull(snaps.get(1).getPriority()); + } + + // Note: skipping test with nested transactions + + @Test + public void testResultingSnapshotIsPassedToOnComplete() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + // Add an event listener at this node so we hang on to local state + // in-between transaction + // runs + ref1.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref1.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue("hello!"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals("hello!", currentData.getValue()); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + + // Do it again for the aborted case + ref1.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + return Transaction.abort(); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertFalse(committed); + assertEquals("hello!", currentData.getValue()); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + + // Now on a fresh connection... + ref2.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() == null) { + currentData.setValue("hello!"); + return Transaction.success(currentData); + } + return Transaction.abort(); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertFalse(committed); + assertEquals("hello!", currentData.getValue()); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testTransactionsAbortAfter25Retries() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + TestHelpers.setHijackHash(ref, true); + final AtomicInteger retries = new AtomicInteger(0); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + int tries = retries.getAndIncrement(); + assertTrue(tries < 25); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNotNull(error); + assertEquals(DatabaseError.MAX_RETRIES, error.getCode()); + assertFalse(committed); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + assertEquals(25, retries.get()); + TestHelpers.setHijackHash(ref, false); + } + + @Test + public void testSetCancelAlreadySentTransactions() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + final Semaphore semaphore = new Semaphore(0); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, 5).timedGet(); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + assertNull(currentData.getValue()); + currentData.setValue(72); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertEquals(DatabaseError.OVERRIDDEN_BY_SET, error.getCode()); + assertFalse(committed); + semaphore.release(1); + } + }); + + ref.setValue(32); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testUpdateShouldNotCancelUnrelatedTransactions() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore fooTransaction = new Semaphore(0); + final Semaphore barTransaction = new Semaphore(0); + + new WriteFuture(ref.child("foo"), 5).timedGet(); + TestHelpers.setHijackHash(ref, true); + + // This transaction should get cancelled as we update "foo" later on. + ref.child("foo").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + // Sleep to prevent too many retries due to hash hijacking. + Thread.sleep(10); + } catch (InterruptedException ignore) { // NOLINT + } + currentData.setValue(72); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertEquals(DatabaseError.OVERRIDDEN_BY_SET, error.getCode()); + assertFalse(committed); + fooTransaction.release(); + } + }); + + // This transaction should not get cancelled since we don't update "bar". + ref.child("bar").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + try { + // Sleep to prevent too many retries due to hash hijacking. + Thread.sleep(10); + } catch (InterruptedException ignore) { // NOLINT + } + currentData.setValue(72); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + barTransaction.release(); + } + }); + + ref.updateChildren( + fromSingleQuotedString( + "{'foo': 'newValue', 'boo': 'newValue', 'loo' : {'doo' : {'boo': 'newValue'}}}"), + new CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + assertTrue("Should have gotten cancelled before the update", + fooTransaction.availablePermits() > 0); + assertTrue("Should run after the update", barTransaction.availablePermits() == 0); + TestHelpers.setHijackHash(ref, false); + } + }); + + TestHelpers.waitFor(barTransaction); + } + + @Test + public void testTransactionsOnUnicodeData() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + final Semaphore semaphore = new Semaphore(0); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, "♜♞♝♛♚♝♞♜").timedGet(); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() != null) { + assertEquals("♜♞♝♛♚♝♞♜", currentData.getValue()); + } + currentData.setValue("♖♘♗♕♔♗♘♖"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals("♖♘♗♕♔♗♘♖", currentData.getValue()); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testImmediatelyAbortingTransaction() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore semaphore = new Semaphore(0); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + return Transaction.abort(); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertFalse(committed); + semaphore.release(1); + } + }); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testAddToAnArrayWithTransaction() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + final Semaphore semaphore = new Semaphore(0); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, Arrays.asList("cat", "horse")).timedGet(); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + Object val = currentData.getValue(); + if (val != null) { + @SuppressWarnings("unchecked") + List update = (List) val; + update.add("dog"); + currentData.setValue(update); + } else { + currentData.setValue(Collections.singletonList("dog")); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + ArrayList expected = new ArrayList<>(3); + expected.addAll(Arrays.asList("cat", "horse", "dog")); + Object result = currentData.getValue(); + TestHelpers.assertDeepEquals(expected, result); + semaphore.release(1); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testMergedTransactionsHaveCorrectSnapshotInOnComplete() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + final Semaphore semaphore = new Semaphore(0); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final String nodeName = ref.getKey(); + new WriteFuture(ref, new MapBuilder().put("a", 0).build()).timedGet(); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + Object val = currentData.getValue(); + if (val != null) { + Map expected = new MapBuilder().put("a", 0L).build(); + TestHelpers.assertDeepEquals(expected, val); + } + currentData.setValue(new MapBuilder().put("a", 1L).build()); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + assertEquals(nodeName, currentData.getKey()); + Object val = currentData.getValue(); + // Per new behavior, will include the accepted value of the transaction, + // if + // it was + // successful. + Map expected = new MapBuilder().put("a", 1L).build(); + TestHelpers.assertDeepEquals(expected, val); + semaphore.release(1); + } + }); + + ref.child("a").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + Object val = currentData.getValue(); + if (val != null) { + assertEquals(1L, val); + } + currentData.setValue(2); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + assertEquals("a", currentData.getKey()); + assertEquals(2L, currentData.getValue()); + semaphore.release(1); + } + }); + TestHelpers.waitFor(semaphore, 2); + } + + // Note: skipping tests for reentrant API calls + + @Test + public void testPendingTransactionsAreCancelledOnDisconnect() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + final Semaphore semaphore = new Semaphore(0); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + new WriteFuture(ref, "initial").timedGet(); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue("new"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertFalse(committed); + assertEquals(DatabaseError.DISCONNECTED, error.getCode()); + semaphore.release(1); + } + }); + + DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.interrupt(ctx); + RepoManager.resume(ctx); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testTransactionWithLocalEvents1() throws InterruptedException { + final Semaphore semaphore = new Semaphore(0); + final Semaphore completeSemaphore = new Semaphore(0); + final List results = new ArrayList<>(); + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + results.add(snapshot); + if (results.size() == 1) { + semaphore.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + TestHelpers.waitFor(semaphore); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue("hello!"); + semaphore.release(1); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + try { + TestHelpers.waitFor(semaphore); + assertTrue(committed); + completeSemaphore.release(1); + } catch (InterruptedException e) { + fail("Should not throw"); + } + } + }, false); + + TestHelpers.waitFor(semaphore); + assertEquals(1, results.size()); + assertNull(results.get(0).getValue()); + // Let the completion handler run + semaphore.release(1); + TestHelpers.waitFor(completeSemaphore); + + assertEquals(2, results.size()); + assertEquals("hello!", results.get(1).getValue()); + } + + @Test + @Ignore + public void testTransactionWithoutLocalEvents2() + throws InterruptedException, TestFailure, ExecutionException, TimeoutException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference ref1 = refs.get(0); + DatabaseReference ref2 = refs.get(1); + + final Semaphore done = new Semaphore(0); + final List events = new ArrayList<>(); + TestHelpers.setHijackHash(ref1, true); + ref1.setValue(0, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + done.release(); + } + }); + TestHelpers.waitFor(done); + + ref1.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + events.add(snapshot); + if (events.size() == 1) { + done.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + TestHelpers.waitFor(done); + + final AtomicInteger retries = new AtomicInteger(0); + ref1.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + retries.getAndIncrement(); + Object val = currentData.getValue(); + if (val.equals(3L)) { + // Will take effect the next time + TestHelpers.setHijackHash(ref1, false); + } + currentData.setValue("txn result"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals("txn result", currentData.getValue()); + done.release(1); + } + }, false); + + for (int i = 0; i < 4; ++i) { + new WriteFuture(ref2, i).timedGet(); + } + + TestHelpers.waitFor(done); + + assertTrue(retries.get() > 1); + int size = events.size(); + assertEquals("txn result", events.get(size - 1).getValue()); + // Note: this test doesn't map cleanly, there is some potential for race + // conditions. + // Check that end state is what we want + } + + // NOTE: skipping test for reentrant API calls + + @Test + public void testTransactionRunsOnNullOnlyOnceAfterReconnectCase1981() + throws TestFailure, ExecutionException, TimeoutException, InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + new WriteFuture(ref, 42).timedGet(); + + DatabaseReference newRef = FirebaseDatabase.getInstance(masterApp).getReference(ref.getKey()); + + final Semaphore done = new Semaphore(0); + final AtomicInteger runs = new AtomicInteger(0); + newRef.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + int run = runs.incrementAndGet(); + if (run == 1) { + assertNull(currentData.getValue()); + } else if (run == 2) { + assertEquals(42L, currentData.getValue()); + } else { + fail("Too many calls"); + return Transaction.abort(); + } + currentData.setValue(3.14); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals(2, runs.get()); + assertEquals(3.14, currentData.getValue()); + done.release(1); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testTransactionRespectsPriority() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore done = new Semaphore(0); + final List values = new ArrayList<>(); + ref.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + values.add(snapshot); + } + + @Override + public void onCancelled(DatabaseError error) { + fail("Should not be cancelled"); + } + }); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue(5); + currentData.setPriority(5); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + done.release(1); + } + }); + + TestHelpers.waitFor(done); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + assertEquals(5L, currentData.getValue()); + assertEquals(5.0, currentData.getPriority()); + currentData.setValue(10); + currentData.setPriority(10); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + done.release(1); + } + }); + + TestHelpers.waitFor(done); + + assertEquals(5L, values.get(values.size() - 2).getValue()); + assertEquals(5.0, values.get(values.size() - 2).getPriority()); + assertEquals(10L, values.get(values.size() - 1).getValue()); + assertEquals(10.0, values.get(values.size() - 1).getPriority()); + } + + @Test + public void testTransactionRevertsDataWhenAddingDeeperListen() throws InterruptedException { + final List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + + final Semaphore gotTest = new Semaphore(0); + refs.get(0).child("y").setValue("test", new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + refs.get(1).runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() == null) { + currentData.child("x").setValue(5); + return Transaction.success(currentData); + } else { + return Transaction.abort(); + } + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + } + }); + + refs.get(1).child("y").addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + if ("test".equals(snapshot.getValue())) { + gotTest.release(1); + } + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + } + }); + TestHelpers.waitFor(gotTest); + } + + @Test + public void testTransactionWithNumericKeys() throws InterruptedException { + final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore done = new Semaphore(0); + + Map initial = new MapBuilder().put("1", 1L).put("5", 5L).put("10", 5L) + .put("20", 20L).build(); + + ref.setValue(initial, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue(42); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + assertNull(error); + done.release(1); + } + }); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testRemoveChildWithPriority() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore done = new Semaphore(0); + + long value = 1378744239756L; + new WriteFuture(ref, value, 0).timedGet(); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue(null); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertTrue(committed); + assertNull(currentData.getValue()); + assertNull(error); + done.release(1); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testUserCodeExceptionsAbortTheTransaction() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore done = new Semaphore(0); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + throw new NullPointerException("lol! user code!"); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertFalse(committed); + assertEquals(DatabaseError.USER_CODE_EXCEPTION, error.getCode()); + done.release(1); + } + }); + + TestHelpers.waitFor(done); + + // Now try it with a Throwable, rather than exception + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + throw new StackOverflowError(); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertFalse(committed); + assertEquals(DatabaseError.USER_CODE_EXCEPTION, error.getCode()); + done.release(1); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testBubbleAppTransactionBug() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore done = new Semaphore(0); + ref.child("a").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue(1); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + } + }); + + ref.child("a").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() != null) { + currentData.setValue((Long) currentData.getValue() + 42); + } else { + currentData.setValue(42); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + } + }); + + ref.child("b").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue(7); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + } + }); + + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() != null) { + currentData.setValue( + (Long) currentData.child("a").getValue() + (Long) currentData.child("b").getValue()); + } else { + currentData.setValue("dummy"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals(50L, currentData.getValue()); + done.release(1); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testLocalServerValuesEventuallyButNotImmediatelyMatchServer() + throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + DatabaseReference writer = refs.get(0); + DatabaseReference reader = refs.get(1); + + final Semaphore completionSemaphore = new Semaphore(0); + final List readSnaps = new ArrayList<>(); + final List writeSnaps = new ArrayList<>(); + + reader.addValueEventListener(new ValueEventListener() { + @Override + public void onCancelled(DatabaseError error) { + } + + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + readSnaps.add(snapshot); + completionSemaphore.release(); + } + } + }); + + writer.addValueEventListener(new ValueEventListener() { + @Override + public void onCancelled(DatabaseError error) { + } + + @Override + public void onDataChange(DataSnapshot snapshot) { + if (snapshot.getValue() != null) { + writeSnaps.add(snapshot); + completionSemaphore.release(); + } + } + }); + + // Go offline for a few ms to make sure we get a different timestamp than + // the server. + writer.getDatabase().goOffline(); + writer.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue(ServerValue.TIMESTAMP); + currentData.setPriority(ServerValue.TIMESTAMP); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + if (error != null || !committed) { + fail("Transaction should succeed"); + } + } + }); + + Thread.sleep(5); + writer.getDatabase().goOnline(); + + TestHelpers.waitFor(completionSemaphore, 4); + + assertEquals(2, readSnaps.size()); + assertEquals(2, writeSnaps.size()); + assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(0).getValue()) < 6000); + assertTrue( + Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(0).getPriority()) < 6000); + assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(1).getValue()) < 6000); + assertTrue( + Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(1).getPriority()) < 6000); + assertFalse(writeSnaps.get(0).getValue().equals(writeSnaps.get(1).getValue())); + assertFalse(writeSnaps.get(0).getPriority().equals(writeSnaps.get(1).getPriority())); + assertEquals(writeSnaps.get(1).getValue(), readSnaps.get(1).getValue()); + assertEquals(writeSnaps.get(1).getPriority(), readSnaps.get(1).getPriority()); + } + + @Test + public void testTransactionWithQueryListen() throws InterruptedException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + final Semaphore semaphore = new Semaphore(0); + + ref.setValue(new MapBuilder().put("a", 1).put("b", 2).build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + ref.limitToFirst(1).addChildEventListener(new ChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + } + + @Override + public void onChildMoved(DataSnapshot snapshot, String previousChildName) { + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + ref.child("a").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, + DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals(1L, currentData.getValue()); + semaphore.release(); + } + }); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testTransactionDoesNotPickUpCachedDataFromPrevious() throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference me = refs.get(0); + final DatabaseReference other = refs.get(1); + + final Semaphore semaphore = new Semaphore(0); + + me.setValue("not null", new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore); + + me.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + semaphore.release(); + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + + TestHelpers.waitFor(semaphore); + + other.setValue(null, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore); + + me.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() == null) { + currentData.setValue("it was null!"); + } else { + currentData.setValue("it was not null!"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals("it was null!", currentData.getValue()); + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testTransactionDoesNotPickUpCachedDataFromPreviousTransaction() + throws InterruptedException { + List refs = IntegrationTestUtils.getRandomNode(masterApp, 2); + final DatabaseReference me = refs.get(0); + final DatabaseReference other = refs.get(1); + + final Semaphore semaphore = new Semaphore(0); + + me.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue("not null"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore); + + other.setValue(null, new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore); + + me.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + if (currentData.getValue() == null) { + currentData.setValue("it was null!"); + } else { + currentData.setValue("it was not null!"); + } + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + assertEquals("it was null!", currentData.getValue()); + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testTransactionOnQueriedLocationDoesNotRunInitiallyOnNull() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + DatabaseReference child = ref.push(); + final Map initialData = new MapBuilder().put("a", 1L).put("b", 2L).build(); + new WriteFuture(child, initialData).timedGet(); + + final Semaphore semaphore = new Semaphore(0); + + ChildEventListener listener = ref.limitToFirst(1) + .addChildEventListener(new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + snapshot.getRef().runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + Assert.assertEquals(initialData, currentData.getValue()); + currentData.setValue(null); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, + DataSnapshot currentData) { + Assert.assertNull(error); + Assert.assertTrue(committed); + Assert.assertNull(currentData.getValue()); + semaphore.release(); + } + }); + } + + @Override + public void onChildRemoved(DataSnapshot snapshot) { + } + }); + + TestHelpers.waitFor(semaphore); + + // cleanup + ref.removeEventListener(listener); + } + + @Test + public void testTransactionsRaiseCorrectChildChangedEventsOnQueries() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + Map value = new MapBuilder() + .put("foo", new MapBuilder().put("value", 1).build()).build(); + new WriteFuture(ref, value).timedGet(); + + final List snapshots = new ArrayList<>(); + + ChildEventListener listener = new TestChildEventListener() { + @Override + public void onChildAdded(DataSnapshot snapshot, String previousChildName) { + snapshots.add(snapshot); + } + + @Override + public void onChildChanged(DataSnapshot snapshot, String previousChildName) { + snapshots.add(snapshot); + } + }; + + ref.endAt(Double.MIN_VALUE).addChildEventListener(listener); + + final Semaphore semaphore = new Semaphore(0); + + ref.child("foo").runTransaction(new Transaction.Handler() { + + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.child("value").setValue(2); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNull(error); + assertTrue(committed); + semaphore.release(); + } + }, false); + + TestHelpers.waitFor(semaphore); + + Assert.assertEquals(2, snapshots.size()); + DataSnapshot addedSnapshot = snapshots.get(0); + Assert.assertEquals("foo", addedSnapshot.getKey()); + Assert.assertEquals(new MapBuilder().put("value", 1L).build(), addedSnapshot.getValue()); + DataSnapshot changedSnapshot = snapshots.get(1); + Assert.assertEquals("foo", changedSnapshot.getKey()); + Assert.assertEquals(new MapBuilder().put("value", 2L).build(), changedSnapshot.getValue()); + + // cleanup + ref.removeEventListener(listener); + } + + @Test + public void testTransactionsUseLocalMerges() + throws InterruptedException, ExecutionException, TimeoutException, TestFailure { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + // Go offline to ensure the update doesn't complete before the transaction + // runs. + DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp); + RepoManager.interrupt(ctx); + ref.updateChildren(new MapBuilder().put("foo", "bar").build()); + ref.child("foo").runTransaction(new Transaction.Handler() { + + @Override + public Transaction.Result doTransaction(MutableData currentData) { + assertEquals("bar", currentData.getValue()); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertEquals(null, error); + assertTrue(committed); + semaphore.release(); + } + }); + + RepoManager.resume(ctx); + TestHelpers.waitFor(semaphore); + } + + @Test + public void testOutOfOrderRemoveWrites() + throws InterruptedException, ExecutionException, TestFailure, TimeoutException { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + ref.setValue(new MapBuilder().put("foo", "bar").build()); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue("transaction-1"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + } + }); + ref.runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue("transaction-2"); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + } + }); + final Semaphore done = new Semaphore(0); + // This will trigger an abort of the transaction which should not cause the + // client to crash + ref.updateChildren(new MapBuilder().put("qux", "quu").build(), + new DatabaseReference.CompletionListener() { + @Override + public void onComplete(DatabaseError error, DatabaseReference ref) { + done.release(); + } + }); + + TestHelpers.waitFor(done); + } + + @Test + public void testReturningNullReturnsNullPointerException() throws Throwable { + DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp); + + final Semaphore semaphore = new Semaphore(0); + + ref.child("foo").runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + return null; + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + assertNotNull(error); + semaphore.release(); + } + }); + + TestHelpers.waitFor(semaphore); + } + + @Test + public void testUnsentTransactionsAreNotCancelledOnDisconnect() throws InterruptedException { + // Hack: To trigger us to disconnect before restoring state, we inject a bad + // auth token. + // In real-world usage the much more common case is that we get redirected + // to a different + // server, but that's harder to manufacture from a test. + final DatabaseConfig cfg = TestHelpers.getDatabaseConfig(masterApp); + cfg.setAuthTokenProvider(new AuthTokenProvider() { + private int count = 0; + + @Override + public void getToken(boolean forceRefresh, final GetTokenCompletionListener listener) { + // Return "bad-token" once to trigger a disconnect, and then a null + // token. + @SuppressWarnings("unused") + Future possiblyIgnoredError = TestHelpers.getExecutorService(cfg) + .schedule(new Runnable() { + @Override + public void run() { + if (count == 0) { + count++; + listener.onSuccess("bad-token"); + } else { + listener.onSuccess(null); + } + } + }, 10, TimeUnit.MILLISECONDS); + } + + @Override + public void addTokenChangeListener(TokenChangeListener listener) { + } + + @Override + public void removeTokenChangeListener(TokenChangeListener listener) { + } + }); + + // Queue a transaction offline. + DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference(); + ref.getDatabase().goOffline(); + final Semaphore semaphore = new Semaphore(0); + ref.push().runTransaction(new Transaction.Handler() { + @Override + public Transaction.Result doTransaction(MutableData currentData) { + currentData.setValue(42); + return Transaction.success(currentData); + } + + @Override + public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) { + + assertNull(error); + assertTrue(committed); + semaphore.release(); + } + }); + + ref.getDatabase().goOnline(); + TestHelpers.waitFor(semaphore); + } +} diff --git a/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java b/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java index 76ac3a944..ce2b121ce 100644 --- a/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java +++ b/src/test/java/com/google/firebase/internal/FirebaseAppStoreTest.java @@ -23,29 +23,6 @@ public class FirebaseAppStoreTest { @Rule public FirebaseAppRule firebaseAppRule = new FirebaseAppRule(); - // TODO(arondeak): reenable persistence. See b/28158809. - // @Test - // public void persistThenRestoreOneApp() { - // String name = "myApp"; - // FirebaseApp.initializeApp(mTargetContext, ALL_VALUES_OPTIONS, name); - // FirebaseAppStore appStore = new SharedPrefsFirebaseAppStore(mTargetContext) - // assertThat(appStore.getAllPersistedAppNames().contains(name)).isTrue(); - // FirebaseApp.clearInstancesForTest(); - // FirebaseApp restoredApp = Iterables.getOnlyElement(FirebaseApp.getApps - // (mTargetContext)); - // assertThat(restoredApp.getOptions()).isEqualTo(ALL_VALUES_OPTIONS); - // } - // - // @Test - // public void persistThenRemoveOneApp() { - // String name = "myApp"; - // FirebaseApp.initializeApp(mTargetContext, ALL_VALUES_OPTIONS, name); - // FirebaseAppStore appStore = new SharedPrefsFirebaseAppStore(mTargetContext); - // assertThat(appStore.getAllPersistedAppNames().contains(name)).isTrue(); - // appStore.removeApp(name); - // assertThat(appStore.getAllPersistedAppNames()).doesNotContain(name); - // } - @Test public void compatibleAppInitializedInNextRunOk() { String name = "myApp"; diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java index b67a426a1..496bad92f 100644 --- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java +++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java @@ -10,7 +10,6 @@ import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.internal.GetTokenResult; import com.google.firebase.internal.Preconditions; -import com.google.firebase.tasks.Continuation; import com.google.firebase.tasks.Task; import com.google.firebase.tasks.Tasks; import java.io.ByteArrayInputStream; @@ -31,8 +30,6 @@ public class IntegrationTestUtils { - public static final long ASYNC_WAIT_TIME_MS = 5000; - private static JSONObject IT_SERVICE_ACCOUNT; public static String getDatabaseUrl() { @@ -65,10 +62,6 @@ public static String getProjectId() { return ensureServiceAccount().get("project_id").toString(); } - public static String getProjectNumber() { - return ensureServiceAccount().get("client_id").toString(); - } - public static FirebaseApp initDefaultApp() { FirebaseOptions options = new FirebaseOptions.Builder() @@ -78,15 +71,31 @@ public static FirebaseApp initDefaultApp() { return FirebaseApp.initializeApp(options); } + public static FirebaseApp initApp(String name) { + FirebaseOptions options = + new FirebaseOptions.Builder() + .setDatabaseUrl(getDatabaseUrl()) + .setCredential(FirebaseCredentials.fromCertificate(getServiceAccountCertificate())) + .build(); + return FirebaseApp.initializeApp(options, name); + } + public static DatabaseReference getRandomNode(FirebaseApp app) { - return getRandomNodes(app, 1).get(0); + return getRandomNode(app, 1).get(0); } - public static List getRandomNodes(FirebaseApp app, int count) { + public static List getRandomNode(FirebaseApp app, int count) { FirebaseDatabase database = FirebaseDatabase.getInstance(app); ImmutableList.Builder builder = ImmutableList.builder(); + String name = null; for (int i = 0; i < count; i++) { - builder.add(database.getReference().push()); + if (name == null) { + DatabaseReference ref = database.getReference().push(); + builder.add(ref); + name = ref.getKey(); + } else { + builder.add(database.getReference().child(name)); + } } return builder.build(); } diff --git a/src/test/java/com/google/firebase/testing/TestUtils.java b/src/test/java/com/google/firebase/testing/TestUtils.java index 99d70ffdd..808e7a484 100644 --- a/src/test/java/com/google/firebase/testing/TestUtils.java +++ b/src/test/java/com/google/firebase/testing/TestUtils.java @@ -15,15 +15,10 @@ import java.util.List; import java.util.Map; -/** Test Utils for use by all tests. */ +/** Test Utils for use by all tests (both unit and integration tests). */ public class TestUtils { - // These constants are also used for the ServiceAccounts. - public static final String PROJECT_ID = "mock-project-id"; - public static final String PROJECT_NUMBER = "1234567890"; - - // Time to wait for async tests to finish. - public static final long ASYNC_WAIT_TIME_MS = 2000; + public static final long TEST_TIMEOUT_MILLIS = 7 * 1000; public static boolean verifySignature(JsonWebSignature token, List keys) throws Exception { @@ -57,7 +52,7 @@ public static void setEnvironmentVariables(Map vars) { } } - public static String loadResource(String path) { + static String loadResource(String path) { InputStream stream = TestUtils.class.getClassLoader().getResourceAsStream(path); checkNotNull(stream, "Failed to load resource: " + path); try (InputStreamReader reader = new InputStreamReader(stream)) {