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
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