/* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.firebase.database.core; import static com.google.common.base.Preconditions.checkState; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.io.CharStreams; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.InternalHelpers; import com.google.firebase.database.Query; import com.google.firebase.database.annotations.NotNull; import com.google.firebase.database.connection.ListenHashProvider; import com.google.firebase.database.core.persistence.NoopPersistenceManager; import com.google.firebase.database.core.utilities.TestClock; import com.google.firebase.database.core.view.Change; import com.google.firebase.database.core.view.DataEvent; import com.google.firebase.database.core.view.Event; import com.google.firebase.database.core.view.QuerySpec; 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.testing.ServiceAccount; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SyncPointTest { private static final Logger logger = LoggerFactory.getLogger(SyncPointTest.class); private static FirebaseApp testApp; @BeforeClass public static void setUpClass() throws IOException { testApp = FirebaseApp.initializeApp( new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); } @AfterClass public static void tearDownClass() { TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } private static SyncTree.ListenProvider getNewListenProvider() { return new SyncTree.ListenProvider() { private final HashSet<QuerySpec> listens = new HashSet<>(); @Override public void startListening( QuerySpec query, Tag tag, ListenHashProvider hash, SyncTree.CompletionListener onListenComplete) { checkState(!listens.contains(query), "Duplicate listen"); this.listens.add(query); } @Override public void stopListening(QuerySpec query, Tag tag) { Path path = query.getPath(); logger.debug("Listening at {} for Tag {}", path, tag); checkState(this.listens.contains(query), "Stopped listening for query already"); this.listens.remove(query); } }; } private static EventRegistration getTestEventRegistration(QuerySpec query) { return new TestEventRegistration(query); } private static void assertEventExactMatch(List<TestEvent> expected, List<TestEvent> actual) { if (expected.size() < actual.size()) { Assert.fail("Got extra events: " + actual); } else if (expected.size() > actual.size()) { Assert.fail("Missing events: " + expected); } else { Iterator<TestEvent> expectedIterator = expected.iterator(); Iterator<TestEvent> actualIterator = actual.iterator(); while (expectedIterator.hasNext() && actualIterator.hasNext()) { TestEvent.assertEquals(expectedIterator.next(), actualIterator.next()); } Assert.assertFalse(expectedIterator.hasNext()); Assert.assertFalse(actualIterator.hasNext()); } } private static void checkOrder(Map<Object, List<TestEvent>> eventsAtPath) { for (List<TestEvent> events : eventsAtPath.values()) { Event.EventType currentEventType = null; for (TestEvent event : events) { if (currentEventType != null) { Assert.assertTrue( "Events should be ordered!", currentEventType.ordinal() <= event.getEventType().ordinal()); } currentEventType = event.getEventType(); } } } private static void assertEventSetsMatch( List<TestEvent> expectedList, List<TestEvent> actualList) { List<TestEvent> currentExpected = new ArrayList<>(expectedList); List<TestEvent> currentActual = new ArrayList<>(actualList); for (TestEvent actual : currentActual) { Iterator<TestEvent> expectedIterator = currentExpected.iterator(); boolean found = false; while (expectedIterator.hasNext()) { TestEvent expected = expectedIterator.next(); if (TestEvent.eventsEqual(actual, expected)) { found = true; expectedIterator.remove(); break; } } Assert.assertTrue( "Expected events did not contain actual event: " + actual + "\nExpected: " + expectedList, found); } Assert.assertTrue("Missing expected events: " + currentExpected, currentExpected.isEmpty()); Path currentPath = null; Map<Object, List<TestEvent>> currentPathRegistrationMap = new HashMap<>(); List<TestEvent> allHandled = new ArrayList<>(); for (TestEvent currentEvent : actualList) { if (!currentEvent.getPath().equals(currentPath)) { checkOrder(currentPathRegistrationMap); currentPathRegistrationMap = new HashMap<>(); currentPath = currentEvent.getPath(); } if (!currentPathRegistrationMap.containsKey(currentEvent.eventRegistration)) { currentPathRegistrationMap.put(currentEvent.eventRegistration, new ArrayList<TestEvent>()); } List<TestEvent> registrationList = currentPathRegistrationMap.get(currentEvent.eventRegistration); registrationList.add(currentEvent); allHandled.add(currentEvent); } checkOrder(currentPathRegistrationMap); // make sure we actually Assert.assertEquals(actualList, allHandled); } @SuppressWarnings("unchecked") private static Query parseQuery(Query query, Map<String, Object> querySpec) { if (!querySpec.containsKey("tag")) { throw new RuntimeException("Non-default queries must have a tag"); } Map<String, Object> remaining = new HashMap<>(querySpec); if (remaining.containsKey("orderBy")) { String key = (String) remaining.get("orderBy"); query = query.orderByChild(key); remaining.remove("orderBy"); } else if (remaining.containsKey("orderByKey")) { query = query.orderByKey(); remaining.remove("orderByKey"); } else if (remaining.containsKey("orderByPriority")) { query = query.orderByPriority(); remaining.remove("orderByPriority"); } if (remaining.containsKey("startAt")) { Map<String, Object> startAt = (Map<String, Object>) remaining.get("startAt"); Object index = startAt.get("index"); if (index == null || index instanceof String) { query = query.startAt((String) index, (String) startAt.get("name")); } else if (index instanceof Boolean) { query = query.startAt((Boolean) index, (String) startAt.get("name")); } else if (index instanceof Double) { query = query.startAt((Double) index, (String) startAt.get("name")); } else if (index instanceof Integer) { query = query.startAt((Integer) index, (String) startAt.get("name")); } else { throw new IllegalArgumentException("Unknown type for index: " + index.getClass()); } remaining.remove("startAt"); } if (remaining.containsKey("endAt")) { Map<String, Object> endAt = (Map<String, Object>) remaining.get("endAt"); Object index = endAt.get("index"); if (index == null || index instanceof String) { query = query.endAt((String) index, (String) endAt.get("name")); } else if (index instanceof Boolean) { query = query.endAt((Boolean) index, (String) endAt.get("name")); } else if (index instanceof Double) { query = query.endAt((Double) index, (String) endAt.get("name")); } else if (index instanceof Integer) { query = query.endAt((Integer) index, (String) endAt.get("name")); } else { throw new IllegalArgumentException("Unknown type for index: " + index.getClass()); } remaining.remove("endAt"); } if (remaining.containsKey("equalTo")) { Map<String, Object> equalTo = (Map<String, Object>) remaining.get("equalTo"); Object index = equalTo.get("index"); if (index == null || index instanceof String) { query = query.equalTo((String) index, (String) equalTo.get("name")); } else if (index instanceof Boolean) { query = query.equalTo((Boolean) index, (String) equalTo.get("name")); } else if (index instanceof Double) { query = query.equalTo((Double) index, (String) equalTo.get("name")); } else { throw new IllegalArgumentException("Unknown type for index: " + index.getClass()); } remaining.remove("equalTo"); } if (remaining.containsKey("limitToFirst")) { query = query.limitToFirst((Integer) remaining.get("limitToFirst")); remaining.remove("limitToFirst"); } if (remaining.containsKey("limitToLast")) { query = query.limitToLast((Integer) remaining.get("limitToLast")); remaining.remove("limitToLast"); } remaining.remove("tag"); if (!remaining.isEmpty()) { throw new RuntimeException("Unsupported query parameters: " + remaining); } return query; } private static TestEvent parseEvent( DatabaseReference ref, Map<String, Object> eventSpec, String basePath) { String path = (String) eventSpec.get("path"); Event.EventType type; String eventTypeStr = (String) eventSpec.get("type"); if (eventTypeStr.equals("value")) { type = Event.EventType.VALUE; } else if (eventTypeStr.equals("child_added")) { type = Event.EventType.CHILD_ADDED; } else if (eventTypeStr.equals("child_moved")) { type = Event.EventType.CHILD_MOVED; } else if (eventTypeStr.equals("child_removed")) { type = Event.EventType.CHILD_REMOVED; } else if (eventTypeStr.equals("child_changed")) { type = Event.EventType.CHILD_CHANGED; } else { throw new RuntimeException("Unknown event type: " + eventTypeStr); } String childName = (String) eventSpec.get("name"); String prevName = eventSpec.get("prevName") != null ? (String) eventSpec.get("prevName") : null; Object data = eventSpec.get("data"); DatabaseReference rootRef = basePath != null ? ref.getRoot().child(basePath) : ref.getRoot(); DatabaseReference pathRef = rootRef.child(path); if (childName != null) { pathRef = pathRef.child(childName); } Node node = NodeUtilities.NodeFromJSON(data); // TODO: don't use priority index by default DataSnapshot snapshot = InternalHelpers.createDataSnapshot(pathRef, IndexedNode.from(node)); return new TestEvent(type, snapshot, prevName, null); } @SuppressWarnings("unchecked") private static List<TestEvent> testEvents(List<? extends Event> events) { return Collections.checkedList((List) events, TestEvent.class); } private static Tag parseTag(Object tag) { return new Tag((Integer) tag); } private static Map<Path, Node> parseMergePaths(Map<String, Object> merges) { Map<Path, Node> newMerges = new HashMap<>(); for (Map.Entry<String, Object> merge : merges.entrySet()) { newMerges.put(new Path(merge.getKey()), NodeUtilities.NodeFromJSON(merge.getValue())); } return newMerges; } @SuppressWarnings("unchecked") private static void runTest(Map<String, Object> testSpec, String basePath) { logger.debug("Running \"{}\"", testSpec.get("name")); SyncTree.ListenProvider listenProvider = getNewListenProvider(); SyncTree syncTree = new SyncTree(new NoopPersistenceManager(), listenProvider); int currentWriteId = 0; List<Map<String, Object>> steps = (List<Map<String, Object>>) testSpec.get("steps"); Map<Integer, EventRegistration> registrations = new HashMap<>(); for (Map<String, Object> spec : steps) { String pathStr = (String) spec.get("path"); Path path = pathStr != null ? new Path(basePath != null ? basePath : "").child(new Path(pathStr)) : null; DatabaseReference reference = InternalHelpers.createReference(null, path); String type = (String) spec.get("type"); List<Map<String, Object>> eventSpecs = (List<Map<String, Object>>) spec.get("events"); List<TestEvent> expected = new ArrayList<>(); for (Map<String, Object> eventSpec : eventSpecs) { expected.add(parseEvent(reference, eventSpec, basePath)); } if (type.equals("listen")) { Query query = reference; if (spec.containsKey("params")) { query = parseQuery(query, (Map<String, Object>) spec.get("params")); } EventRegistration eventRegistration; Integer callbackId = (Integer) spec.get("callbackId"); if (callbackId != null && registrations.containsKey(callbackId)) { eventRegistration = registrations.get(callbackId); } else { eventRegistration = getTestEventRegistration(query.getSpec()); if (callbackId != null) { registrations.put(callbackId, eventRegistration); } } List<TestEvent> actual = testEvents(syncTree.addEventRegistration(eventRegistration)); assertEventExactMatch(expected, actual); } else if (type.equals("unlisten")) { EventRegistration eventRegistration = null; Integer callbackId = (Integer) spec.get("callbackId"); if (callbackId == null || !registrations.containsKey(callbackId)) { throw new IllegalArgumentException( "Couldn't find previous listen will callbackId " + callbackId); } eventRegistration = registrations.get(callbackId); List<TestEvent> actual = testEvents(syncTree.removeEventRegistration(eventRegistration)); assertEventExactMatch(expected, actual); } else if (type.equals("serverUpdate")) { Node update = NodeUtilities.NodeFromJSON(spec.get("data")); List<TestEvent> actual; if (spec.containsKey("tag")) { actual = testEvents( syncTree.applyTaggedQueryOverwrite(path, update, parseTag(spec.get("tag")))); } else { actual = testEvents(syncTree.applyServerOverwrite(path, update)); } assertEventSetsMatch(expected, actual); } else if (type.equals("serverMerge")) { Map<Path, Node> merges = parseMergePaths((Map<String, Object>) spec.get("data")); List<TestEvent> actual; if (spec.containsKey("tag")) { actual = testEvents(syncTree.applyTaggedQueryMerge(path, merges, parseTag(spec.get("tag")))); } else { actual = testEvents(syncTree.applyServerMerge(path, merges)); } assertEventSetsMatch(expected, actual); } else if (type.equals("set")) { Node toSet = NodeUtilities.NodeFromJSON(spec.get("data")); boolean visible = spec.containsKey("visible") ? (Boolean) spec.get("visible") : true; boolean persist = visible; // for now, assume anything visible should be persisted. List<TestEvent> actual = testEvents( syncTree.applyUserOverwrite( path, toSet, toSet, currentWriteId++, visible, persist)); assertEventSetsMatch(expected, actual); } else if (type.equals("update")) { CompoundWrite merges = CompoundWrite.fromValue((Map<String, Object>) spec.get("data")); List<TestEvent> actual = testEvents(syncTree.applyUserMerge(path, merges, merges, currentWriteId++, true)); assertEventSetsMatch(expected, actual); } else if (type.equals("ackUserWrite")) { int toClear = (Integer) spec.get("writeId"); boolean revert = spec.containsKey("revert") ? (Boolean) spec.get("revert") : false; List<TestEvent> actual = testEvents(syncTree.ackUserWrite(toClear, revert, true, new TestClock())); assertEventSetsMatch(expected, actual); } else if (type.equals("suppressWarning")) { // Do nothing. This is a hack so JS's Jasmine tests don't throw warnings for // "expect no // errors" tests. } else { throw new RuntimeException("Unknown step: " + type); } } } @SuppressWarnings("unchecked") private List<Map<String, Object>> loadSpecs() { String pathToResource = "syncPointSpec.json"; InputStream stream = getClass().getClassLoader().getResourceAsStream(pathToResource); if (stream == null) { throw new RuntimeException("Failed to find syncPointSpec.json resource."); } try (InputStreamReader reader = new InputStreamReader(stream)) { return (List) JsonMapper.parseJsonValue(CharStreams.toString(reader)); } catch (IOException e) { throw new RuntimeException(e); } } @Test public void runAll() { List<Map<String, Object>> specs = loadSpecs(); for (Map<String, Object> spec : specs) { runTest(spec, null); // Run again at a deeper path runTest(spec, "/foo/bar/baz"); } } public void runOne(String name) { List<Map<String, Object>> specs = loadSpecs(); for (Map<String, Object> spec : specs) { if (name.equals(spec.get("name"))) { runTest(spec, null); // Run again at a deeper path runTest(spec, "/foo/bar/baz"); return; } } throw new RuntimeException("Didn't find test spec with name " + name); } @Test public void defaultListenHandlesParentSet() { runOne("Default listen handles a parent set"); } @Test public void defaultListenHandlesASetAtTheSameLevel() { runOne("Default listen handles a set at the same level"); } @Test public void testQueryCanGetCompleteCacheThenMerge() { runOne("A query can get a complete cache then a merge"); } @Test public void serverMergeOnListenerWithCompleteChildren() { runOne("Server merge on listener with complete children"); } @Test public void deepMergeOnListenerWithCompleteChildren() { runOne("Deep merge on listener with complete children"); } @Test public void updateChildListenerTwice() { runOne("Update child listener twice"); } @Test public void childOfDefaultListenThatAlreadyHasACompleteCache() { runOne("Update child of default listen that already has a complete cache"); } @Test public void updateChildOfDefaultListenThatHasNoCache() { runOne("Update child of default listen that has no cache"); } @Test public void updateTheChildOfACoLocatedDefaultListenerAndQuery() { runOne("Update (via set) the child of a co-located default listener and query"); } @Test public void updateTheChildOfAQueryWithAFullCache() { runOne("Update (via set) the child of a query with a full cache"); } @Test public void updateAChildBelowAnEmptyQuery() { runOne("Update (via set) a child below an empty query"); } @Test public void updateDescendantOfDefaultListenerWithFullCache() { runOne("Update descendant of default listener with full cache"); } @Test public void descendantSetBelowAnEmptyDefaultLIstenerIsIgnored() { runOne("Descendant set below an empty default listener is ignored"); } @Test public void updateOfAChild() { runOne("Update of a child. This can happen if a child listener is added and removed"); } @Test public void revertSetWithOnlyChildCaches() { runOne("Revert set with only child caches"); } @Test public void canRevertADuplicateChildSet() { runOne("Can revert a duplicate child set"); } @Test public void canRevertAChildSetAndSeeTheUnderlyingData() { runOne("Can revert a child set and see the underlying data"); } @Test public void revertChildSetWithNoServerData() { runOne("Revert child set with no server data"); } @Test public void revertDeepSetWithNoServerData() { runOne("Revert deep set with no server data"); } @Test public void revertSetCoveredByNonvisibleTransaction() { runOne("Revert set covered by non-visible transaction"); } @Test public void clearParentShadowingServerValuesSetWithServerChildren() { runOne("Clear parent shadowing server values set with server children"); } @Test public void clearChildShadowingServerValuesSetWithServerChildren() { runOne("Clear child shadowing server values set with server children"); } @Test public void unrelatedMergeDoesntShadowServerUpdates() { runOne("Unrelated merge doesn't shadow server updates"); } @Test public void canSetAlongsideARemoteMerge() { runOne("Can set alongside a remote merge"); } @Test public void setPriorityOnALocationWithNoCache() { runOne("setPriority on a location with no cache"); } @Test public void deepUpdateDeletesChildFromLimitWindowAndPullsInNewChild() { runOne("deep update deletes child from limit window and pulls in new child"); } @Test public void deepSetDeletesChildFromLimitWindowAndPullsInNewChild() { runOne("deep set deletes child from limit window and pulls in new child"); } @Test public void edgeCaseInNewChildForChange() { runOne("Edge case in newChildForChange_"); } @Test public void revertSetInQueryWindow() { runOne("Revert set in query window"); } @Test public void handlesAServerValueMovingAChildOutOfAQueryWindow() { runOne("Handles a server value moving a child out of a query window"); } @Test public void updateOfIndexedChildWorks() { runOne("Update of indexed child works"); } @Test public void mergeAppliedToEmptyLimit() { runOne("Merge applied to empty limit"); } @Test public void limitIsRefilledFromServerDataAfterMerge() { runOne("Limit is refilled from server data after merge"); } @Test public void handleRepeatedListenWithMergeAsFirstUpdate() { runOne("Handle repeated listen with merge as first update"); } @Test public void limitIsRefilledFromServerDataAfterSet() { runOne("Limit is refilled from server data after set"); } @Test public void queryOnWeirdPath() { runOne("query on weird path."); } @Test public void runsRound2() { runOne("runs, round2"); } @Test public void handlesNestedListens() { runOne("handles nested listens"); } @Test public void handlesASetBelowAListen() { runOne("Handles a set below a listen"); } @Test public void doesNonDefaultQueries() { runOne("does non-default queries"); } @Test public void handlesCoLocatedDefaultListenerAndQuery() { runOne("handles a co-located default listener and query"); } @Test public void defaultAndNonDefaultListenerAtSameLocationWithServerUpdate() { runOne("Default and non-default listener at same location with server update"); } @Test public void addAParentListenerToACompleteChildListenerExpectChildEvent() { runOne("Add a parent listener to a complete child listener, expect child event"); } @Test public void addListensToASetExpectCorrectEventsIncludingAChildEvent() { runOne("Add listens to a set, expect correct events, including a child event"); } @Test public void serverUpdateToAChildListenerRaisesChildEventsAtParent() { runOne("ServerUpdate to a child listener raises child events at parent"); } @Test public void serverUpdateToAChildListenerRaisesChildEventsAtParentQuery() { runOne("ServerUpdate to a child listener raises child events at parent query"); } @Test public void multipleCompleteChildrenAreHandleProperly() { runOne("Multiple complete children are handled properly"); } @Test public void writeLeafNodeOverwriteAtParentNode() { runOne("Write leaf node, overwrite at parent node"); } @Test public void confirmCompleteChildrenFromTheServer() { runOne("Confirm complete children from the server"); } @Test public void writeLeafOverwriteFromParent() { runOne("Write leaf, overwrite from parent"); } @Test public void basicUpdateTest() { runOne("Basic update test"); } @Test public void noDoubleValueEventsForUserAck() { runOne("No double value events for user ack"); } @Test public void basicKeyIndexSanityCheck() { runOne("Basic key index sanity check"); } @Test public void collectCorrectSubviewsToListenOn() { runOne("Collect correct subviews to listen on"); } @Test public void limitToFirstOneOnOrderedQuery() { runOne("Limit to first one on ordered query"); } @Test public void limitToLastOneOnOrderedQuery() { runOne("Limit to last one on ordered query"); } @Test public void updateIndexedValueOnExistingChildFromLimitedQuery() { runOne("Update indexed value on existing child from limited query"); } @Test public void canCreateStartAtEndAtEqualToQueriesWithBool() { runOne("Can create startAt, endAt, equalTo queries with bool"); } @Test public void queryForExistingServerSnap() { runOne("Query with existing server snap"); } @Test public void serverDataIsNotPurgedForNonServerIndexedQueries() { runOne("Server data is not purged for non-server-indexed queries"); } @Test public void limitWithCustomOrderByIsRefilledWithCorrectItem() { runOne("Limit with custom orderBy is refilled with correct item"); } @Test public void startAtEndAtDominatesLimit() { runOne("startAt/endAt dominates limit"); } @Test public void updateToSingleChildThatMovesOutOfWindow() { runOne("Update to single child that moves out of window"); } @Test public void limitedQueryDoesntPullInOutOfRangeChild() { runOne("Limited query doesn't pull in out of range child"); } @Test public void mergerForLocationWithDefaultAndLimitedListener() { runOne("Merge for location with default and limited listener"); } @Test public void userMergePullsInCorrectValues() { runOne("User merge pulls in correct values"); } @Test public void userDeepSetPullsInCorrectValues() { runOne("User deep set pulls in correct values"); } @Test public void queriesWithEqualToNullWork() { runOne("Queries with equalTo(null) work"); } @Test public void revertedWritesUpdateQuery() { runOne("Reverted writes update query"); } @Test public void deepSetForNonLocalDataDoesntRaiseEvents() { runOne("Deep set for non-local data doesn't raise events"); } @Test public void userUpdateWithNewChildrenTriggersEvents() { runOne("User update with new children triggers events"); } @Test public void userWriteWithDeepOverwrite() { runOne("User write with deep user overwrite"); } @Test public void deepServerMerge() { runOne("Deep server merge"); } @Test public void serverUpdatesPriority() { runOne("Server updates priority"); } @Test public void revertFullUnderlyingWrite() { runOne("Revert underlying full overwrite"); } @Test public void userChildOverwriteForNonexistentServerNode() { runOne("User child overwrite for non-existent server node"); } @Test public void revertUserOverwriteOfChildOnLeafNode() { runOne("Revert user overwrite of child on leaf node"); } @Test public void serverOverwriteWithDeepUserDelete() { runOne("Server overwrite with deep user delete"); } @Test public void userOverwritesLeafNodeWithPriority() { runOne("User overwrites leaf node with priority"); } @Test public void userOverwritesInheritPriorityValuesFromLeafNodes() { runOne("User overwrites inherit priority values from leaf nodes"); } @Test public void userUpdateOnUserSetLeafNodeWithPriorityAfterServerUpdate() { runOne("User update on user set leaf node with priority after server update"); } @Test public void serverDeepDeleteOnLeafNode() { runOne("Server deep delete on leaf node"); } @Test public void userSetsRootPriority() { runOne("User sets root priority"); } @Test public void userUpdatesPriorityOnEmptyRoot() { runOne("User updates priority on empty root"); } @Test public void revertSetAtRootWithPriority() { runOne("Revert set at root with priority"); } @Test public void serverUpdatesPriorityAfterUserSetsPriority() { runOne("Server updates priority after user sets priority"); } @Test public void emptySetDoesntPreventServerUpdates() { runOne("Empty set doesn't prevent server updates"); } @Test public void userUpdatesPriorityTwiceFirstIsReverted() { runOne("User updates priority twice, first is reverted"); } @Test public void serverAcksRootPrioritySetAfterUserDeletesRootNode() { runOne("Server acks root priority set after user deletes root node"); } @Test public void deleteInAMergeDoesntPushOutNodes() { runOne("A delete in a merge doesn't push out nodes"); } @Test public void taggedQueryFiresEventsEventually() { runOne("A tagged query fires events eventually"); } @Test public void serverUpdateThatLeavesUserSetsUnchangedIsNotIgnored() { runOne("A server update that leaves user sets unchanged is not ignored"); } @Test public void userWriteOutsideOfLimitIsIgnoredForTaggedQueries() { runOne("User write outside of limit is ignored for tagged queries"); } @Test public void ackForMergeDoesntRaiseValueEventForLaterListen() { runOne("Ack for merge doesn't raise value event for later listen"); } @Test public void clearParentShadowingServerValuesMergeWithServerChildren() { runOne("Clear parent shadowing server values merge with server children"); } @Test public void prioritiesDontMakeMeSick() { runOne("Priorities don't make me sick"); } @Test public void mergeThatMovesChildFromWindowToBoundaryDoesNotCauseChildToBeReadded() { runOne("Merge that moves child from window to boundary does not cause child to be readded"); } @Test public void deepMergeAckIsHandledCorrectly() { runOne("Deep merge ack is handled correctly."); } @Test public void deepMergeAckOnIncompleteDataAndWithServerValues() { runOne("Deep merge ack (on incomplete data, and with server values)"); } @Test public void limitQueryHandlesDeepServerMergeForOutOfViewItem() { runOne("Limit query handles deep server merge for out-of-view item."); } @Test public void limitQueryHandlesDeepUserMergeForOutOfViewItem() { runOne("Limit query handles deep user merge for out-of-view item."); } @Test public void limitQueryHandlesDeepUserMergeForOutOfViewItemFollowedByServerUpdate() { runOne( "Limit query handles deep user merge for out-of-view item followed by server " + "update."); } @Test public void unrelatedUntaggedUpdateIsNotCachedInTaggedListen() { runOne("Unrelated, untagged update is not cached in tagged listen"); } @Test public void unrelatedAckedSetIsNotCachedInTaggedListen() { runOne("Unrelated, acked set is not cached in tagged listen"); } @Test public void unrelatedAckedUpdateIsNotCachedInTaggedListen() { runOne("Unrelated, acked update is not cached in tagged listen"); } @Test public void deepUpdateRaisesImmediateEventsOnlyIfHasCompleteData() { runOne("Deep update raises immediate events only if has complete data"); } @Test public void deepUpdateReturnsMinimumDataRequired() { runOne("Deep update returns minimum data required"); } @Test public void deepUpdateRaisesAllEvents() { runOne("Deep update raises all events"); } private static class TestEvent extends DataEvent { private final EventType eventType; private final DataSnapshot snapshot; private final Object eventRegistration; public TestEvent( EventType eventType, DataSnapshot snapshot, String prevName, Object eventRegistration) { super(eventType, null, snapshot, prevName); this.eventType = eventType; this.snapshot = snapshot; this.eventRegistration = eventRegistration; } private static boolean equalsOrNull(Object one, Object two) { if (one == null) { return two == null; } else { return one.equals(two); } } public static boolean eventsEqual(TestEvent one, TestEvent other) { if (!one.getPath().equals(other.getPath())) { return false; } if (!equalsOrNull(one.getSnapshot().getKey(), other.getSnapshot().getKey())) { return false; } if (!equalsOrNull(one.getSnapshot().getPriority(), other.getSnapshot().getPriority())) { return false; } if (!equalsOrNull(one.getSnapshot().getValue(), other.getSnapshot().getValue())) { return false; } if (one.getPreviousName() == null) { return other.getPreviousName() == null; } else { return one.getPreviousName().equals(other.getPreviousName()); } } public static void assertEquals(TestEvent expectedEvent, TestEvent actualEvent) { Assert.assertEquals(expectedEvent.getPreviousName(), actualEvent.getPreviousName()); Assert.assertEquals(expectedEvent.getPath(), actualEvent.getPath()); Assert.assertEquals( expectedEvent.getSnapshot().getValue(true), actualEvent.getSnapshot().getValue(true)); } @Override public Path getPath() { Path path = this.snapshot.getRef().getPath(); if (this.eventType == EventType.VALUE) { return path; } else { return path.getParent(); } } @Override public void fire() { throw new UnsupportedOperationException("Event doesn't support event runner for TestEvents"); } } private static class TestEventRegistration extends EventRegistration { private QuerySpec query; public TestEventRegistration(QuerySpec query) { this.query = query; } @Override public boolean respondsTo(Event.EventType eventType) { return true; } @Override public DataEvent createEvent(Change change, QuerySpec query) { DataSnapshot snapshot; if (change.getEventType() == Event.EventType.VALUE) { snapshot = InternalHelpers.createDataSnapshot( InternalHelpers.createReference(null, query.getPath()), change.getIndexedNode()); } else { snapshot = InternalHelpers.createDataSnapshot( InternalHelpers.createReference(null, query.getPath().child(change.getChildKey())), change.getIndexedNode()); } String prevName = change.getPrevName() != null ? change.getPrevName().asString() : null; return new TestEvent(change.getEventType(), snapshot, prevName, this); } @Override public void fireEvent(DataEvent dataEvent) { throw new UnsupportedOperationException("Can't raise test events!"); } @Override public void fireCancelEvent(DatabaseError error) { throw new UnsupportedOperationException("Can't raise test events!"); } @Override public EventRegistration clone(QuerySpec newQuery) { return new TestEventRegistration(newQuery); } @Override public boolean isSameListener(EventRegistration other) { return other == this; } @NotNull @Override public QuerySpec getQuerySpec() { return query; } } }