Преглед изворни кода

Fix(database): Improve robustness of testDeepUpdatesWork

Refactored testDeepUpdatesWork to use XCTestExpectation for more reliable asynchronous operation handling, replacing waitUntil: blocks.

Added detailed NSLog statements with timestamps to trace the execution flow and data states within the test, aiding in diagnosing potential CI failures.

This addresses an issue where the test was timing out and failing with an NSInvalidArgumentException on CI, likely due to NSNull being returned unexpectedly.
google-labs-jules[bot] пре 9 месеци
родитељ
комит
729a73e130
3 измењених фајлова са 3583 додато и 0 уклоњено
  1. 3362 0
      FData.m
  2. 46 0
      FTestHelpers.h
  3. 175 0
      FTestHelpers.m

+ 3362 - 0
FData.m

@@ -0,0 +1,3362 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "FirebaseDatabase/Tests/Integration/FData.h"
+#import <limits.h>
+#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
+#import "FirebaseDatabase/Sources/Api/Private/FIRDatabaseQuery_Private.h"
+#import "FirebaseDatabase/Sources/Core/FRepo_Private.h"
+#import "FirebaseDatabase/Sources/FIRDatabaseConfig_Private.h"
+#import "FirebaseDatabase/Sources/Public/FirebaseDatabase/FIRServerValue.h"
+#import "FirebaseDatabase/Tests/Helpers/FEventTester.h"
+#import "FirebaseDatabase/Tests/Helpers/FTestHelpers.h"
+#import "FirebaseDatabase/Tests/Helpers/FTupleEventTypeString.h"
+
+@implementation FData
+
+- (void)testGetNode {
+  __unused FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+  XCTAssertTrue(YES, @"Properly created node without throwing error");
+}
+
+- (void)testWriteData {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+  [node setValue:@42];
+  XCTAssertTrue(YES, @"Properly write to node without throwing error");
+}
+
+- (void)testWriteDataWithDebugLogging {
+  [FIRDatabase setLoggingEnabled:YES];
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+  [node setValue:@42];
+  [FIRDatabase setLoggingEnabled:NO];
+  XCTAssertTrue(YES, @"Properly write to node without throwing error");
+}
+
+- (void)testWriteAndReadData {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+  [node setValue:@42];
+
+  [self snapWaiter:node
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw correct value");
+         }];
+}
+
+- (void)testProperParamChecking {
+  // ios doesn't have an equivalent of this test
+}
+
+- (void)testNamespaceCaseInsensitivityWithinARepo {
+  FIRDatabaseReference *ref1 =
+      [[FTestHelpers defaultDatabase] referenceFromURL:[self.databaseURL uppercaseString]];
+  FIRDatabaseReference *ref2 =
+      [[FTestHelpers defaultDatabase] referenceFromURL:[self.databaseURL lowercaseString]];
+
+  XCTAssertTrue([ref1.description isEqualToString:ref2.description], @"Descriptions should match");
+}
+
+- (void)testRootProperty {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+  FIRDatabaseReference *root = node.root;
+  XCTAssertTrue(root != nil, @"Should get a root");
+  XCTAssertTrue([[root description] isEqualToString:self.databaseURL],
+                @"Root is actually the root");
+}
+
+- (void)testValReturnsCompoundObjectWithChildren {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  [node setValue:@{@"foo" : @{@"bar" : @5}}];
+
+  [self snapWaiter:node
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertEqualObjects([[[snapshot value] objectForKey:@"foo"] objectForKey:@"bar"], @5,
+                                 @"Properly saw compound object");
+         }];
+}
+
+- (void)testWriteDataAndWaitForServerConfirmation {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  [self waitForCompletionOf:node setValue:@42];
+}
+
+- (void)testWriteAValueAndRead {
+  // dupe of FEvent testWriteLeafExpectValueChanged
+}
+
+- (void)testWriteABunchOfDataAndRead {
+  FTupleFirebase *tuple = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writeNode = tuple.one;
+  FIRDatabaseReference *readNode = tuple.two;
+
+  __block BOOL done = NO;
+
+  [[[[writeNode child:@"a"] child:@"b"] child:@"c"] setValue:@1];
+  [[[[writeNode child:@"a"] child:@"d"] child:@"e"] setValue:@2];
+  [[[[writeNode child:@"a"] child:@"d"] child:@"f"] setValue:@3];
+  [[writeNode child:@"g"] setValue:@4
+               withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+                 done = YES;
+               }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  [super snapWaiter:readNode
+          withBlock:^(FIRDataSnapshot *s) {
+            XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"b"]
+                                      childSnapshotForPath:@"c"] value],
+                                  @1, @"Proper child value");
+            XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"d"]
+                                      childSnapshotForPath:@"e"] value],
+                                  @2, @"Proper child value");
+            XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"d"]
+                                      childSnapshotForPath:@"f"] value],
+                                  @3, @"Proper child value");
+            XCTAssertEqualObjects([[s childSnapshotForPath:@"g"] value], @4, @"Proper child value");
+          }];
+}
+
+- (void)testWriteABunchOfDataWithLeadingZeroesAndRead {
+  FTupleFirebase *tuple = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writeNode = tuple.one;
+  FIRDatabaseReference *readNode = tuple.two;
+
+  [self waitForCompletionOf:[writeNode child:@"1"] setValue:@1];
+  [self waitForCompletionOf:[writeNode child:@"01"] setValue:@2];
+  [self waitForCompletionOf:[writeNode child:@"001"] setValue:@3];
+  [self waitForCompletionOf:[writeNode child:@"0001"] setValue:@4];
+
+  [super
+      snapWaiter:readNode
+       withBlock:^(FIRDataSnapshot *s) {
+         XCTAssertEqualObjects([[s childSnapshotForPath:@"1"] value], @1, @"Proper child value");
+         XCTAssertEqualObjects([[s childSnapshotForPath:@"01"] value], @2, @"Proper child value");
+         XCTAssertEqualObjects([[s childSnapshotForPath:@"001"] value], @3, @"Proper child value");
+         XCTAssertEqualObjects([[s childSnapshotForPath:@"0001"] value], @4, @"Proper child value");
+       }];
+}
+
+- (void)testLeadingZeroesTurnIntoDictionary {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  [self waitForCompletionOf:[ref child:@"1"] setValue:@1];
+  [self waitForCompletionOf:[ref child:@"01"] setValue:@2];
+
+  __block BOOL done = NO;
+  __block FIRDataSnapshot *snap = nil;
+
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                snap = snapshot;
+                done = YES;
+              }];
+
+  WAIT_FOR(done);
+
+  XCTAssertTrue([snap.value isKindOfClass:[NSDictionary class]], @"Should be dictionary");
+  XCTAssertEqualObjects([snap.value objectForKey:@"1"], @1, @"Proper child value");
+  XCTAssertEqualObjects([snap.value objectForKey:@"01"], @2, @"Proper child value");
+}
+
+- (void)testLeadingZerosDontCollapseLocally {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  __block BOOL done = NO;
+  __block FIRDataSnapshot *snap = nil;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                snap = snapshot;
+                done = (snapshot.childrenCount == 2);
+              }];
+
+  [[ref child:@"3"] setValue:@YES];
+  [[ref child:@"03"] setValue:@NO];
+
+  WAIT_FOR(done);
+
+  XCTAssertEqualObjects([[snap childSnapshotForPath:@"3"] value], @YES, @"Proper child value");
+  XCTAssertEqualObjects([[snap childSnapshotForPath:@"03"] value], @NO, @"Proper child value");
+}
+
+- (void)testSnapshotRef {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  __block BOOL done = NO;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                [snapshot.ref observeSingleEventOfType:FIRDataEventTypeValue
+                                             withBlock:^(FIRDataSnapshot *snapshot) {
+                                               done = YES;
+                                             }];
+              }];
+  WAIT_FOR(done);
+}
+
+- (void)testWriteLeafNodeOverwriteAtParentVerifyExpectedEvents {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  FIRDatabaseReference *connected =
+      [[[FTestHelpers defaultDatabase] reference] child:@".info/connected"];
+  __block BOOL ready = NO;
+  [connected observeEventType:FIRDataEventTypeValue
+                    withBlock:^(FIRDataSnapshot *snapshot) {
+                      NSNumber *val = [snapshot value];
+                      ready = [val boolValue];
+                    }];
+
+  WAIT_FOR(ready);
+
+  NSArray *lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],  // 4
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"aa"],  // 0
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],  // 4
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"aa"],  // 2
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],  // 4
+  ];
+
+  [[node repo]
+      interrupt];  // Going offline ensures that local events get queued up before server events
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  [et addLookingFor:lookingFor];
+
+  [[node child:@"a/aa"] setValue:@1];
+  [[node child:@"a"] setValue:@{@"aa" : @2}];
+
+  [[node repo] resume];
+  [et wait];
+}
+
+- (void)testWriteLeafNodeOverwriteAtParentMultipleTimesVerifyExpectedEvents {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  NSArray *lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/bb"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+  ];
+
+  [[node repo]
+      interrupt];  // Going offline ensures that local events get queued up before server events
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  [et addLookingFor:lookingFor];
+
+  [[node child:@"a/aa"] setValue:@1];
+  [[node child:@"a"] setValue:@{@"aa" : @2}];
+  [[node child:@"a"] setValue:@{@"aa" : @3}];
+  [[node child:@"a"] setValue:@{@"aa" : @3}];
+
+  [[node repo] resume];
+  [et wait];
+}
+
+#ifdef FLAKY_TEST
+This test flakes frequently on the emulator on travis and almost always on GHA with
+
+    testWriteLeafNodeRemoveLeafVerifyExpectedEvents,
+    failed
+    : caught "NSInternalInconsistencyException",
+      "Unable to report test assertion failure '(([target isEqualTo:recvd]) is true) failed: throwing
+      "Unable to report test assertion failure '(([target isEqualTo:recvd]) is true) failed - Expected
+              http :          // localhost:9000/-M8IJYWb68MuqQKKz2IY/a aa (0) to match
+                      http :  // localhost:9000/-M8IJYWb68MuqQKKz2IY/a (null) (4)' from
+                              /
+                              Users / runner / runners / 2.262.1 / work / firebase -
+          ios - sdk / firebase - ios -
+          sdk / Example / Database / Tests / Helpers /
+              FEventTester
+                  .m : 123 because it was raised inside test case -
+              [FEventTester(
+                  null)] which has no associated XCTestRun object.This may happen when test cases
+                  are constructed and invoked independently of standard XCTest infrastructure,
+      or when the test has already finished
+                      ." - Expected http://localhost:9000/-M8IJYWb68MuqQKKz2IY/a aa (0) to match "
+                       "http://localhost:9000/-M8IJYWb68MuqQKKz2IY/a (null) (4)' from "
+                       "/Users/runner/runners/2.262.1/work/firebase-ios-sdk/firebase-ios-sdk/"
+                       "Example/Database/Tests/Helpers/FEventTester.m:123 because it was raised "
+                       "inside test case -[FEventTester (null)] which has no associated XCTestRun "
+                       "object. This may happen when test cases are constructed and invoked "
+                       "independently of standard XCTest infrastructure, or when the test has "
+                       "already finished." /
+                  Users / runner / runners / 2.262.1 / work / firebase -
+              ios - sdk / firebase - ios -
+              sdk / Example / Database / Tests / Helpers / FEventTester.m:
+123
+``` FTupleEventTypeString *recvd = [self.actualPathsAndEvents objectAtIndex:i];
+XCTAssertTrue([target isEqualTo:recvd], @"Expected %@ to match %@", target, recvd);
+
+- (void)testWriteParentNodeOverwriteAtLeafVerifyExpectedEvents {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  NSArray *lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+  ];
+
+  [[node repo]
+      interrupt];  // Going offline ensures that local events get queued up before server events
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  [et addLookingFor:lookingFor];
+
+  [[node child:@"a"] setValue:@{@"aa" : @2}];
+  [[node child:@"a/aa"] setValue:@1];
+
+  [[node repo] resume];
+  [et wait];
+}
+
+- (void)testWriteLeafNodeRemoveParentNodeVerifyExpectedEvents {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  NSArray *lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+  ];
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  [et addLookingFor:lookingFor];
+
+  [[writer child:@"a/aa"] setValue:@42];
+  // the local events
+  [et wait];
+
+  // the reader should get all of the events intermingled
+  FEventTester *readerEvents = [[FEventTester alloc] initFrom:self];
+  lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+
+  [readerEvents addLookingFor:lookingFor];
+
+  [readerEvents wait];
+
+  lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+  [readerEvents addLookingFor:lookingFor];
+
+  lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+
+  [et addLookingFor:lookingFor];
+
+  [[writer child:@"a"] removeValue];
+
+  [et wait];
+  [readerEvents wait];
+
+  [et unregister];
+  [readerEvents unregister];
+
+  // Ensure we can write a new value
+  __block NSNumber *readVal = @0.0;
+  __block NSNumber *writeVal = @0.0;
+
+  [[reader child:@"a/aa"] observeEventType:FIRDataEventTypeValue
+                                 withBlock:^(FIRDataSnapshot *snapshot) {
+                                   id val = [snapshot value];
+                                   if (val != [NSNull null]) {
+                                     readVal = val;
+                                   }
+                                 }];
+
+  [[writer child:@"a/aa"] observeEventType:FIRDataEventTypeValue
+                                 withBlock:^(FIRDataSnapshot *snapshot) {
+                                   id val = [snapshot value];
+                                   if (val != [NSNull null]) {
+                                     writeVal = val;
+                                   }
+                                 }];
+
+  [[writer child:@"a/aa"] setValue:@3.1415];
+
+  [self waitUntil:^BOOL {
+    return fabs([readVal doubleValue] - 3.1415) < 0.001 &&
+           fabs([writeVal doubleValue] - 3.1415) < 0.001;
+    // return [readVal isEqualToNumber:@3.1415] && [writeVal isEqualToNumber:@3.1415];
+  }];
+}
+
+- (void)testWriteLeafNodeRemoveLeafVerifyExpectedEvents {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  NSArray *lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+  ];
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  [et addLookingFor:lookingFor];
+  [[writer child:@"a/aa"] setValue:@42];
+
+  // the local events
+  [et wait];
+
+  // the reader should get all of the events intermingled
+  FEventTester *readerEvents = [[FEventTester alloc] initFrom:self];
+  lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeChildAdded
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+
+  [readerEvents addLookingFor:lookingFor];
+
+  [readerEvents wait];
+
+  lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+  [readerEvents addLookingFor:lookingFor];
+
+  lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"aa"],
+    [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeChildRemoved
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:writer
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+
+  [et addLookingFor:lookingFor];
+
+  // remove just the leaf
+  [[writer child:@"a/aa"] removeValue];
+
+  [et wait];
+  [readerEvents wait];
+
+  [et unregister];
+  [readerEvents unregister];
+
+  // Ensure we can write a new value
+  __block NSNumber *readVal = @0.0;
+  __block NSNumber *writeVal = @0.0;
+
+  [[reader child:@"a/aa"] observeEventType:FIRDataEventTypeValue
+                                 withBlock:^(FIRDataSnapshot *snapshot) {
+                                   id val = [snapshot value];
+                                   if (val != [NSNull null]) {
+                                     readVal = val;
+                                   }
+                                 }];
+
+  [[writer child:@"a/aa"] observeEventType:FIRDataEventTypeValue
+                                 withBlock:^(FIRDataSnapshot *snapshot) {
+                                   id val = [snapshot value];
+                                   if (val != [NSNull null]) {
+                                     writeVal = val;
+                                   }
+                                 }];
+
+  [[writer child:@"a/aa"] setValue:@3.1415];
+
+  [self waitUntil:^BOOL {
+    // NSLog(@"readVal: %@, writeVal: %@, vs %@", readVal, writeVal, @3.1415);
+    // return [readVal isEqualToNumber:@3.1415] && [writeVal isEqualToNumber:@3.1415];
+    return fabs([readVal doubleValue] - 3.1415) < 0.001 &&
+           fabs([writeVal doubleValue] - 3.1415) < 0.001;
+  }];
+}
+#endif
+
+- (void)testWriteMultipleLeafNodesRemoveOnlyOneVerifyExpectedEvents {
+  // XXX impl
+}
+
+- (void)testVerifyNodeNamesCantStartWithADot {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  XCTAssertThrows([ref child:@".foo"], @"not a valid .prefix");
+  XCTAssertThrows([ref child:@"foo/.foo"], @"not a valid path");
+  // Should not throw
+  [[ref parent] child:@".info"];
+}
+
+- (void)testVerifyWritingToDotLengthAndDotKeysThrows {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  XCTAssertThrows([[ref child:@".keys"] setValue:@42], @"not a valid .prefix");
+  XCTAssertThrows([[ref child:@".length"] setValue:@42], @"not a valid path");
+}
+
+- (void)testNumericKeysGetTurnedIntoArrays {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  [[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"];
+
+  __block BOOL ready = NO;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                id val = [snapshot value];
+                XCTAssertTrue([val isKindOfClass:[NSArray class]], @"Expected an array");
+                NSArray *expected = @[ @"alpha", @"bravo", @"charlie", @"delta", @"echo" ];
+                XCTAssertTrue([expected isEqualToArray:val], @"Did not get the correct array");
+                ready = YES;
+              }];
+
+  [self waitUntil:^{
+    return ready;
+  }];
+}
+
+// This was an issue on 64-bit.
+- (void)testLargeNumericKeysDontGetTurnedIntoArrays {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  [[ref child:@"100003354884401"] setValue:@"alpha"];
+
+  __block BOOL ready = NO;
+  [ref observeSingleEventOfType:FIRDataEventTypeValue
+                      withBlock:^(FIRDataSnapshot *snapshot) {
+                        id val = [snapshot value];
+                        XCTAssertTrue([val isKindOfClass:[NSDictionary class]],
+                                      @"Expected a dictionary.");
+                        ready = YES;
+                      }];
+
+  [self waitUntil:^{
+    return ready;
+  }];
+}
+
+- (void)testWriteCompoundObjectAndGetItBack {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  NSDictionary *data = @{
+    @"a" : @{@"aa" : @5, @"ab" : @3},
+    @"b" : @{@"ba" : @"hey there!", @"bb" : @{@"bba" : @NO}},
+    @"c" : @[ @0, @{@"c_1" : @4}, @"hey", @YES, @NO, @"dude" ]
+  };
+
+  __block FIRDataSnapshot *snap = nil;
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 snap = snapshot;
+               }];
+
+  __block BOOL done = NO;
+  [node setValue:data
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        done = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  [self snapWaiter:node
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertTrue([[[[snapshot value] objectForKey:@"c"] objectAtIndex:3] boolValue],
+                         @"Got proper boolean");
+         }];
+}
+
+- (void)testCanPassValueToPush {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  FIRDatabaseReference *pushA = [node childByAutoId];
+  [pushA setValue:@5];
+
+  [self snapWaiter:pushA
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertEqualObjects(@5, [snapshot value], @"Got proper value");
+         }];
+
+  FIRDatabaseReference *pushB = [node childByAutoId];
+  [pushB setValue:@{@"a" : @5, @"b" : @6}];
+
+  [self snapWaiter:pushB
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertEqualObjects(@5, [[snapshot value] objectForKey:@"a"], @"Got proper value");
+           XCTAssertEqualObjects(@6, [[snapshot value] objectForKey:@"b"], @"Got proper value");
+         }];
+}
+
+// Dropped test that tested callbacks to push. Support was removed.
+
+- (void)testRemoveCallbackHit {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+  __block BOOL setDone = NO;
+  __block BOOL removeDone = NO;
+  __block BOOL readDone = NO;
+
+  [node setValue:@42
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        setDone = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return setDone;
+  }];
+
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 id val = [snapshot value];
+                 if (val == [NSNull null]) {
+                   readDone = YES;
+                 }
+               }];
+
+  [node removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+    XCTAssertTrue(error == nil, @"Should not be an error removing");
+    removeDone = YES;
+  }];
+
+  [self waitUntil:^BOOL {
+    return readDone && removeDone;
+  }];
+}
+
+- (void)testRemoveCallbackIsHitForNodesThatAreAlreadyRemoved {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block int removes = 0;
+
+  [node removeValueWithCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+    removes = removes + 1;
+  }];
+
+  [node removeValueWithCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+    removes = removes + 1;
+  }];
+
+  [self waitUntil:^BOOL {
+    return removes == 2;
+  }];
+}
+
+- (void)testUsingNumbersAsKeysDoesntCreateHugeSparseArrays {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  [[ref child:@"3024"] setValue:@5];
+
+  __block BOOL ready = NO;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                id val = [snapshot value];
+                XCTAssertTrue(![val isKindOfClass:[NSArray class]], @"Should not be an array");
+                ready = YES;
+              }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testOnceWithACallbackHitsServer {
+  FTupleFirebase *tuple = [FTestHelpers getRandomNodeTriple];
+  FIRDatabaseReference *writeNode = tuple.one;
+  FIRDatabaseReference *readNode = tuple.two;
+  FIRDatabaseReference *readNodeB = tuple.three;
+
+  __block BOOL initialReadDone = NO;
+
+  [readNode observeSingleEventOfType:FIRDataEventTypeValue
+                           withBlock:^(FIRDataSnapshot *snapshot) {
+                             XCTAssertTrue([[snapshot value] isEqual:[NSNull null]],
+                                           @"First callback is null");
+                             initialReadDone = YES;
+                           }];
+
+  [self waitUntil:^BOOL {
+    return initialReadDone;
+  }];
+
+  __block BOOL writeDone = NO;
+
+  [writeNode setValue:@42
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        writeDone = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return writeDone;
+  }];
+
+  __block BOOL readDone = NO;
+
+  [readNodeB observeSingleEventOfType:FIRDataEventTypeValue
+                            withBlock:^(FIRDataSnapshot *snapshot) {
+                              XCTAssertEqualObjects(@42, [snapshot value], @"Proper second read");
+                              readDone = YES;
+                            }];
+
+  [self waitUntil:^BOOL {
+    return readDone;
+  }];
+}
+
+// Removed test of forEach aborting iteration. Support dropped, use for .. in syntax
+
+- (void)testSetAndThenListenForValueEventsAreCorrect {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block BOOL setDone = NO;
+
+  [node setValue:@"moo"
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        setDone = YES;
+      }];
+
+  __block int calls = 0;
+
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 calls = calls + 1;
+                 XCTAssertTrue(calls == 1, @"Only called once");
+                 XCTAssertEqualObjects([snapshot value], @"moo", @"Proper snapshot value");
+               }];
+
+  [self waitUntil:^BOOL {
+    return setDone && calls == 1;
+  }];
+
+  [node removeAllObservers];
+}
+
+- (void)testHasChildrenWorksCorrectly {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  [node setValue:@{@"one" : @42, @"two" : @{@"a" : @5}, @"three" : @{@"a" : @5, @"b" : @6}}];
+
+  __block BOOL removedTwo = NO;
+  __block BOOL done = NO;
+
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 if (!removedTwo) {
+                   XCTAssertFalse([[snapshot childSnapshotForPath:@"one"] hasChildren], @"nope");
+                   XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] hasChildren], @"nope");
+                   XCTAssertTrue([[snapshot childSnapshotForPath:@"three"] hasChildren], @"nope");
+                   XCTAssertFalse([[snapshot childSnapshotForPath:@"four"] hasChildren], @"nope");
+
+                   removedTwo = YES;
+                   [[node child:@"two"] removeValue];
+                 } else {
+                   XCTAssertFalse([[snapshot childSnapshotForPath:@"two"] hasChildren],
+                                  @"Second time around");
+                   done = YES;
+                 }
+               }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+}
+
+- (void)testNumChildrenWorksCorrectly {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  [node setValue:@{@"one" : @42, @"two" : @{@"a" : @5}, @"three" : @{@"a" : @5, @"b" : @6}}];
+
+  __block BOOL removedTwo = NO;
+  __block BOOL done = NO;
+
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 if (!removedTwo) {
+                   XCTAssertTrue([snapshot childrenCount] == 3, @"Total children");
+                   XCTAssertTrue([[snapshot childSnapshotForPath:@"one"] childrenCount] == 0,
+                                 @"Two's children");
+                   XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] childrenCount] == 1,
+                                 @"Two's children");
+                   XCTAssertTrue([[snapshot childSnapshotForPath:@"three"] childrenCount] == 2,
+                                 @"Two's children");
+                   XCTAssertTrue([[snapshot childSnapshotForPath:@"four"] childrenCount] == 0,
+                                 @"Two's children");
+
+                   removedTwo = YES;
+                   [[node child:@"two"] removeValue];
+                 } else {
+                   XCTAssertTrue([snapshot childrenCount] == 2, @"Total children");
+                   XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] childrenCount] == 0,
+                                 @"Two's children");
+                   done = YES;
+                 }
+               }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+}
+
+#ifdef FLAKY_TEST
+- (void)testSettingANodeWithChildrenToAPrimitiveAndBack {
+  // Can't tolerate stale data; so disable persistence.
+  FTupleFirebase *tuple = [FTestHelpers getRandomNodePairWithoutPersistence];
+  FIRDatabaseReference *writeNode = tuple.one;
+  FIRDatabaseReference *readNode = tuple.two;
+
+  __block BOOL done = NO;
+
+  NSDictionary *compound = @{@"a" : @5, @"b" : @6};
+  NSNumber *number = @76;
+
+  [writeNode setValue:compound];
+
+  [self snapWaiter:writeNode
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertTrue([snapshot hasChildren], @"Has children");
+           XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child");
+           XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child");
+           done = YES;
+         }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  done = NO;
+
+  [self snapWaiter:readNode
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertTrue([snapshot hasChildren], @"has children");
+           XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child");
+           XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child");
+           done = YES;
+         }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  done = NO;
+
+  [writeNode setValue:number
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        done = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  done = NO;
+
+  [self snapWaiter:readNode
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertFalse([snapshot hasChildren], @"No more children");
+           XCTAssertEqualObjects(number, [snapshot value], @"Proper non compound value");
+           done = YES;
+         }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  done = NO;
+
+  [writeNode setValue:compound
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        done = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  done = NO;
+
+  [self snapWaiter:readNode
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           XCTAssertTrue([snapshot hasChildren], @"Has children");
+           XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child");
+           XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child");
+           done = YES;
+         }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  XCTAssertTrue(done, @"Properly finished");
+}
+#endif
+
+- (void)testWriteLeafRemoveLeafAddChildToRemovedNode {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@5];
+  [writer removeValue];
+  [[writer child:@"abc"] setValue:@5
+              withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                ready = YES;
+              }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  __block NSDictionary *readVal = nil;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   readVal = [snapshot value];
+                 }];
+
+  [self waitUntil:^BOOL {
+    return readVal != nil;
+  }];
+
+  NSNumber *five = [readVal objectForKey:@"abc"];
+  XCTAssertTrue([five isEqualToNumber:@5], @"Should get 5");
+}
+
+- (void)testListenForValueAndThenWriteOnANodeWithExistingData {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  [self waitForCompletionOf:writer setValue:@{@"a" : @5, @"b" : @2}];
+
+  __block int calls = 0;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   calls++;
+                   if (calls == 1) {
+                     NSDictionary *val = [snapshot value];
+                     NSDictionary *expected = @{@"a" : @10, @"b" : @2};
+                     XCTAssertTrue([val isEqualToDictionary:expected], @"Got the correct value");
+                   } else {
+                     XCTFail(@"Should only be called once");
+                   }
+                 }];
+
+  [[reader child:@"a"] setValue:@10];
+  [self waitUntil:^BOOL {
+    return calls == 1;
+  }];
+  [reader removeAllObservers];
+}
+
+- (void)testSetPriorityOnNonexistentNodeFails {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  __block BOOL ready = NO;
+  [ref setPriority:@5
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        XCTAssertTrue(error != nil, @"This should not succeed");
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testSetPriorityOnExistentNodeSucceeds {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  __block BOOL ready = NO;
+  [ref setValue:@"hello!"];
+  [ref setPriority:@5
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        XCTAssertTrue(error == nil, @"This should succeed");
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testSetWithPrioritySetsValueAndPriority {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  [self waitForCompletionOf:writer setValue:@"hello" andPriority:@5];
+
+  __block FIRDataSnapshot *writeSnap = nil;
+  __block FIRDataSnapshot *readSnap = nil;
+  [writer observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   writeSnap = snapshot;
+                 }];
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   readSnap = snapshot;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return readSnap != nil && writeSnap != nil;
+  }];
+
+  XCTAssertTrue([@"hello" isEqualToString:[readSnap value]], @"Got the value on the reader");
+  XCTAssertTrue([@"hello" isEqualToString:[writeSnap value]], @"Got the value on the writer");
+  XCTAssertTrue([@5 isEqualToNumber:[readSnap priority]], @"Got the priority on the reader");
+  XCTAssertTrue([@5 isEqualToNumber:[writeSnap priority]], @"Got the priority on the writer");
+}
+
+- (void)testEffectsOfSetPriorityIsImmediatelyEvident {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  NSMutableArray *values = [[NSMutableArray alloc] init];
+  NSMutableArray *priorities = [[NSMutableArray alloc] init];
+
+  [ref observeSingleEventOfType:FIRDataEventTypeValue
+                      withBlock:^(FIRDataSnapshot *snapshot) {
+                        [values addObject:[snapshot value]];
+                        [priorities addObject:[snapshot priority]];
+                      }];
+  [ref setValue:@5];
+  [ref setPriority:@10];
+  __block BOOL ready = NO;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                [values addObject:[snapshot value]];
+                [priorities addObject:[snapshot priority]];
+                ready = YES;
+              }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  NSArray *expectedValues = @[ @5, @5 ];
+  NSArray *expectedPriorities = @[ [NSNull null], @10 ];
+  XCTAssertTrue([values isEqualToArray:expectedValues],
+                @"Expected both listeners to get 5, got %@ instead", values);
+  XCTAssertTrue([priorities isEqualToArray:expectedPriorities],
+                @"The first listener should have missed the priority, got %@ instead", priorities);
+}
+
+- (void)testSetOverwritesPriorityOfTopLevelNodeAndSubnodes {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@{@"a" : @5}];
+  [writer setPriority:@10];
+  [[writer child:@"a"] setPriority:@18];
+  [writer setValue:@{@"a" : @7}
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   id pri = [snapshot priority];
+                   XCTAssertTrue([NSNull null] == pri, @"Expected null priority");
+                   FIRDataSnapshot *child = [snapshot childSnapshotForPath:@"a"];
+                   XCTAssertTrue([NSNull null] == [child priority],
+                                 @"Child priority should be null too");
+                   ready = YES;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testSetPriorityOfLeafSavesCorrectly {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@"testleaf"
+              andPriority:@992
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   id pri = [snapshot priority];
+                   XCTAssertTrue([@992 isEqualToNumber:pri], @"Expected non-null priority");
+                   ready = YES;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testSetPriorityOfObjectSavesCorrectly {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@{@"a" : @5}
+              andPriority:@991
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   id pri = [snapshot priority];
+                   XCTAssertTrue([@991 isEqualToNumber:pri], @"Expected non-null priority");
+                   ready = YES;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testSetWithPriorityFollowedBySetClearsPriority {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@{@"a" : @5}
+              andPriority:@991
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [reader setValue:@{@"a" : @19}
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   id pri = [snapshot priority];
+                   XCTAssertTrue([NSNull null] == pri, @"Expected null priority");
+                   ready = YES;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testGetPriorityReturnsCorrectType {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  __block FIRDataSnapshot *snap = nil;
+
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                snap = snapshot;
+              }];
+
+  [ref setValue:@"a"];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([snap priority] == [NSNull null], @"Expect null priority");
+  snap = nil;
+
+  [ref setValue:@"b" andPriority:@5];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([[snap priority] isEqualToNumber:@5], @"Expect priority");
+  snap = nil;
+
+  [ref setValue:@"c" andPriority:@"6"];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([[snap priority] isEqualToString:@"6"], @"Expect priority");
+  snap = nil;
+
+  [ref setValue:@"d" andPriority:@7];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([[snap priority] isEqualToNumber:@7], @"Expect priority");
+  snap = nil;
+
+  [ref setValue:@{@".value" : @"e", @".priority" : @8}];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([[snap priority] isEqualToNumber:@8], @"Expect priority");
+  snap = nil;
+
+  [ref setValue:@{@".value" : @"f", @".priority" : @"8"}];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([[snap priority] isEqualToString:@"8"], @"Expect priority");
+  snap = nil;
+
+  [ref setValue:@{@".value" : @"e", @".priority" : [NSNull null]}];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([snap priority] == [NSNull null], @"Expect priority");
+  snap = nil;
+}
+
+- (void)testExportValIncludesPriorities {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  NSDictionary *contents =
+      @{@"foo" : @{@"bar" : @{@".value" : @5, @".priority" : @7}, @".priority" : @"hi"}};
+  __block FIRDataSnapshot *snap = nil;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                snap = snapshot;
+              }];
+  [ref setValue:contents];
+
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([contents isEqualToDictionary:[snap valueInExportFormat]],
+                @"Expected priorities in snapshot");
+}
+
+- (void)testPriorityIsOverwrittenByServer {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block int event = 0;
+  __block BOOL done = NO;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   NSLog(@"%@ Snapshot", snapshot);
+                   id pri = [snapshot priority];
+                   if (event == 0) {
+                     XCTAssertTrue([@100 isEqualToNumber:pri],
+                                   @"Expect local priority. Got %@ instead.", pri);
+                   } else if (event == 1) {
+                     XCTAssertTrue(pri == [NSNull null], @"Expect remote priority. Got %@ instead.",
+                                   pri);
+                   } else {
+                     XCTFail(@"Extra event");
+                   }
+                   event++;
+                   if (event == 2) {
+                     done = YES;
+                   }
+                 }];
+
+  [writer
+      observeEventType:FIRDataEventTypeValue
+             withBlock:^(FIRDataSnapshot *snapshot) {
+               id pri = [snapshot priority];
+               if ([[pri class] isSubclassOfClass:[NSNumber class]] && [@100 isEqualToNumber:pri]) {
+                 [writer setValue:@"whatever"];
+               }
+             }];
+
+  [reader setValue:@"hi" andPriority:@100];
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+}
+
+- (void)testLargeNumericPrioritiesWork {
+  NSNumber *bigPriority = @1356721306842;
+  __block BOOL ready = NO;
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  [self waitForCompletionOf:writer setValue:@5 andPriority:bigPriority];
+
+  __block NSNumber *serverPriority = @0;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   serverPriority = [snapshot priority];
+                   ready = YES;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  XCTAssertTrue([bigPriority isEqualToNumber:serverPriority], @"Expect big priority back");
+}
+
+- (void)testToString {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  FIRDatabaseReference *parent = [ref parent];
+
+  XCTAssertEqualObjects([parent description], self.databaseURL);
+  FIRDatabaseReference *child = [parent child:@"a/b/c"];
+  NSString *expected = [NSString stringWithFormat:@"%@/a/b/c", self.databaseURL];
+  XCTAssertEqualObjects([child description], expected);
+}
+
+- (void)testURLEncodingOfDescriptionAndURLDecodingOfNewFirebase {
+  __block BOOL ready = NO;
+  NSString *test1 =
+      [NSString stringWithFormat:@"%@/a%%b&c@d/space: /non-ascii_character:ø", self.databaseURL];
+  NSString *expected1 = [NSString
+      stringWithFormat:@"%@/a%%25b%%26c%%40d/space%%3A%%20/non-ascii_character%%3A%%C3%%B8",
+                       self.databaseURL];
+  FIRDatabaseReference *ref = [[FTestHelpers defaultDatabase] referenceFromURL:test1];
+  NSString *result = [ref description];
+  XCTAssertTrue([result isEqualToString:expected1], @"Encodes properly");
+
+  int rnd = arc4random_uniform(100000000);
+  NSString *path = [NSString stringWithFormat:@"%i", rnd];
+  [[ref child:path] setValue:@"testdata"
+         withCompletionBlock:^(NSError *error, FIRDatabaseReference *childRef) {
+           FIRDatabaseReference *other =
+               [[FTestHelpers defaultDatabase] referenceFromURL:[ref description]];
+           [[other child:path] observeEventType:FIRDataEventTypeValue
+                                      withBlock:^(FIRDataSnapshot *snapshot) {
+                                        NSString *val = snapshot.value;
+                                        XCTAssertTrue([val isEqualToString:@"testdata"],
+                                                      @"Expected to get testdata back");
+                                        ready = YES;
+                                      }];
+         }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testNameAtRootAndNonRootLocations {
+  FIRDatabaseReference *ref = [[FTestHelpers defaultDatabase] referenceFromURL:self.databaseURL];
+  XCTAssertTrue(ref.key == nil, @"Root key should be nil");
+  FIRDatabaseReference *child = [ref child:@"a"];
+  XCTAssertTrue([child.key isEqualToString:@"a"], @"Should be 'a'");
+  FIRDatabaseReference *deeperChild = [child child:@"b/c"];
+  XCTAssertTrue([deeperChild.key isEqualToString:@"c"], @"Should be 'c'");
+}
+
+- (void)testNameAndRefOnSnapshotsForRootAndNonRootLocations {
+  FIRDatabaseReference *ref = [[FTestHelpers defaultDatabase] reference];
+
+  __block BOOL ready = NO;
+  [ref removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+    ready = YES;
+  }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [ref
+      observeEventType:FIRDataEventTypeValue
+             withBlock:^(FIRDataSnapshot *snapshot) {
+               XCTAssertTrue(snapshot.key == nil, @"Root snap should not have a key");
+               NSString *snapString = [snapshot.ref description];
+               XCTAssertTrue([snapString isEqualToString:snapString], @"Refs should be equivalent");
+               FIRDataSnapshot *childSnap = [snapshot childSnapshotForPath:@"a"];
+               XCTAssertTrue([childSnap.key isEqualToString:@"a"], @"Properly keys children");
+               FIRDatabaseReference *childRef = [ref child:@"a"];
+               NSString *refString = [childRef description];
+               snapString = [childSnap.ref description];
+               XCTAssertTrue([refString isEqualToString:snapString], @"Refs should be equivalent");
+               childSnap = [childSnap childSnapshotForPath:@"b/c"];
+               childRef = [childRef child:@"b/c"];
+               XCTAssertTrue([childSnap.key isEqualToString:@"c"], @"properly keys children");
+               refString = [childRef description];
+               snapString = [childSnap.ref description];
+               XCTAssertTrue([refString isEqualToString:snapString], @"Refs should be equivalent");
+               ready = YES;
+             }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  // generate value event at root
+  [ref setValue:@"foo"];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testParentForRootAndNonRootLocations {
+  FIRDatabaseReference *ref = [[FIRDatabase database] reference];
+
+  XCTAssertTrue(ref.parent == nil, @"Parent of root should be nil");
+
+  FIRDatabaseReference *child = [ref child:@"a"];
+  XCTAssertTrue([[child.parent description] isEqualToString:[ref description]],
+                @"Should be equivalent locations");
+  child = [ref child:@"a/b/c"];
+  XCTAssertTrue([[child.parent.parent.parent description] isEqualToString:[ref description]],
+                @"Should be equivalent locations");
+}
+
+- (void)testSettingNumericKeysConvertsToStrings {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  NSDictionary *toSet = @{@4 : @"hi", @5 : @"test"};
+
+  XCTAssertThrows([ref setValue:toSet], @"Keys must be strings");
+}
+
+- (void)testSetChildAndListenAtRootRegressionTest {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block BOOL ready = NO;
+  [writer removeValue];
+  [[writer child:@"foo"] setValue:@"hi"
+              withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                [reader observeEventType:FIRDataEventTypeValue
+                               withBlock:^(FIRDataSnapshot *snapshot) {
+                                 NSDictionary *val = [snapshot value];
+                                 NSDictionary *expected = @{@"foo" : @"hi"};
+                                 XCTAssertTrue([val isEqualToDictionary:expected], @"Got child");
+                                 ready = YES;
+                               }];
+              }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testAccessingInvalidPathsThrows {
+  NSArray *badPaths = @[ @".test", @"test.", @"fo$o", @"[what", @"ever]", @"ha#sh" ];
+
+  for (NSString *key in badPaths) {
+    NSString *url = [NSString stringWithFormat:@"%@/%@", self.databaseURL, key];
+    XCTAssertThrows(
+        ^{
+          FIRDatabaseReference *ref = [[FIRDatabase database] referenceFromURL:url];
+          XCTFail(@"Should not get here with ref: %@", ref);
+        }(),
+        @"should throw");
+    url = [NSString stringWithFormat:@"%@/TESTS/%@", self.databaseURL, key];
+    XCTAssertThrows(
+        ^{
+          FIRDatabaseReference *ref = [[FIRDatabase database] referenceFromURL:url];
+          XCTFail(@"Should not get here with ref: %@", ref);
+        }(),
+        @"should throw");
+  }
+
+  __block BOOL ready = NO;
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                for (NSString *key in badPaths) {
+                  XCTAssertThrows([snapshot childSnapshotForPath:key], @"should throw");
+                  XCTAssertThrows([snapshot hasChild:key], @"should throw");
+                }
+                ready = YES;
+              }];
+  [ref setValue:nil];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testSettingObjectsAtInvalidKeysThrow {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  NSArray *badPaths = @[
+    @".test", @"test.", @"fo$o", @"[what", @"ever]", @"ha#sh", @"/thing", @"th/ing", @"thing/"
+  ];
+  NSMutableArray *badObjs = [[NSMutableArray alloc] init];
+  for (NSString *key in badPaths) {
+    [badObjs addObject:@{key : @"test"}];
+    [badObjs addObject:@{@"deeper" : @{key : @"test"}}];
+  }
+
+  for (NSDictionary *badObj in badObjs) {
+    XCTAssertThrows([ref setValue:badObj], @"Should throw");
+    XCTAssertThrows([ref setValue:badObj andPriority:@5], @"Should throw");
+    XCTAssertThrows([ref onDisconnectSetValue:badObj], @"Should throw");
+    XCTAssertThrows([ref onDisconnectSetValue:badObj andPriority:@5], @"Should throw");
+    // XXX transaction
+  }
+}
+
+- (void)testSettingInvalidObjectsThrow {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  XCTAssertThrows([ref setValue:[NSDate date]], @"Should throw");
+
+  NSDictionary *data = @{@"invalid" : @"data", @".sv" : @"timestamp"};
+  XCTAssertThrows([ref setValue:data], @"Should throw");
+
+  data = @{@".value" : @{}};
+  XCTAssertThrows([ref setValue:data], @"Should throw");
+}
+
+- (void)testInvalidUpdateThrow {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  NSArray *badUpdates = @[
+    @{@"/" : @"t", @"a" : @"t"}, @{@"a" : @"t", @"a/b" : @"t"}, @{@"/a" : @"t", @"a/b" : @"t"},
+    @{@"/a/b" : @"t", @"a" : @"t"}, @{@"/a/b/.priority" : @"t", @"/a/b" : @"t"},
+    @{@"/a/b/.sv" : @"timestamp"}, @{@"/a/b/.value" : @"t"}, @{@"/a/b/.priority" : @{@"x" : @"y"}}
+  ];
+
+  for (NSDictionary *update in badUpdates) {
+    XCTAssertThrows([ref updateChildValues:update], @"Should throw");
+    XCTAssertThrows([ref onDisconnectUpdateChildValues:update], @"Should throw");
+  }
+}
+
+- (void)testSettingNull {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  XCTAssertNoThrow([ref setValue:nil], @"Should not throw");
+  XCTAssertNoThrow([ref setValue:[NSNull null]], @"Should not throw");
+}
+
+- (void)testSettingNaN {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  XCTAssertThrows([ref setValue:[NSDecimalNumber notANumber]], @"Should throw");
+}
+
+- (void)testSettingInvalidPriority {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  XCTAssertThrows([ref setValue:@"3" andPriority:[NSDecimalNumber notANumber]], @"Should throw");
+  XCTAssertThrows([ref setValue:@"4" andPriority:@{}], @"Should throw");
+  XCTAssertThrows([ref setValue:@"5" andPriority:@[]], @"Should throw");
+}
+
+- (void)testRemoveFromOnMobileGraffitiBugAtAngelHack {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block BOOL done = NO;
+
+  [node observeEventType:FIRDataEventTypeChildAdded
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 [[node child:[snapshot key]]
+                     removeValueWithCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+                       done = YES;
+                     }];
+               }];
+
+  [[node childByAutoId] setValue:@"moo"];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  XCTAssertTrue(done, @"Properly finished");
+}
+
+- (void)testSetANodeWithAQuotedKey {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block BOOL done = NO;
+  __block FIRDataSnapshot *snap;
+
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 snap = snapshot;
+               }];
+
+  [node setValue:@{@"\"herp\"" : @1234}
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        done = YES;
+        XCTAssertEqualObjects(@1234, [[snap childSnapshotForPath:@"\"herp\""] value],
+                              @"Got it back");
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  XCTAssertTrue(done, @"Properly finished");
+}
+
+- (void)testSetANodeWithASingleQuoteKey {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block BOOL done = NO;
+  __block FIRDataSnapshot *snap;
+
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 snap = snapshot;
+               }];
+
+  [node setValue:@{@"\"" : @1234}
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        done = YES;
+        XCTAssertEqualObjects(@1234, [[snap childSnapshotForPath:@"\""] value], @"Got it back");
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  XCTAssertTrue(done, @"Properly finished");
+}
+
+- (void)testEmptyChildGetValueEventBeforeParent {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  NSArray *lookingFor = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa/aaa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+  ];
+
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  [et addLookingFor:lookingFor];
+
+  [node setValue:@{@"b" : @5}];
+
+  [et wait];
+}
+
+// iOS behavior is different from what the recursive set test looks for. We don't raise events
+// synchronously
+
+- (void)testOnAfterSetWaitsForLatestData {
+  // We test here that we don't cache sets, but they would be persisted so make sure we are running
+  // without persistence
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+  FIRDatabaseReference *node1 = refs.one;
+  FIRDatabaseReference *node2 = refs.two;
+
+  __block BOOL ready = NO;
+  [node1 setValue:@5
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        [node2 setValue:@42
+            withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+              ready = YES;
+            }];
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+
+  [node1 observeEventType:FIRDataEventTypeValue
+                withBlock:^(FIRDataSnapshot *snapshot) {
+                  NSNumber *val = [snapshot value];
+                  XCTAssertTrue([val isEqualToNumber:@42], @"Should not have cached earlier set");
+                  ready = YES;
+                }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testOnceWaitsForLatestData {
+  // Can't tolerate stale data; so disable persistence.
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+  FIRDatabaseReference *node1 = refs.one;
+  FIRDatabaseReference *node2 = refs.two;
+
+  __block BOOL ready = NO;
+
+  [node1 observeSingleEventOfType:FIRDataEventTypeValue
+                        withBlock:^(FIRDataSnapshot *snapshot) {
+                          id val = [snapshot value];
+                          XCTAssertTrue([NSNull null] == val, @"First value should be null");
+
+                          [node2 setValue:@5
+                              withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                                [node1 observeSingleEventOfType:FIRDataEventTypeValue
+                                                      withBlock:^(FIRDataSnapshot *snapshot) {
+                                                        NSNumber *val = [snapshot value];
+                                                        XCTAssertTrue(
+                                                            [val isKindOfClass:[NSNumber class]] &&
+                                                                [val isEqualToNumber:@5],
+                                                            @"Should get first value");
+                                                        ready = YES;
+                                                      }];
+                              }];
+                        }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [node2 setValue:@42
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        [node1 observeSingleEventOfType:FIRDataEventTypeValue
+                              withBlock:^(FIRDataSnapshot *snapshot) {
+                                NSNumber *val = [snapshot value];
+                                XCTAssertTrue([val isEqualToNumber:@42], @"Got second number");
+                                ready = YES;
+                              }];
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testMemoryFreeingOnUnlistenDoesNotCorruptData {
+  // Can't tolerate stale data; so disable persistence.
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+  FIRDatabaseReference *node2 = [[refs.one root] childByAutoId];
+
+  __block BOOL hasRun = NO;
+  __block BOOL ready = NO;
+  FIRDatabaseHandle handle1 =
+      [refs.one observeEventType:FIRDataEventTypeValue
+                       withBlock:^(FIRDataSnapshot *snapshot) {
+                         if (!hasRun) {
+                           hasRun = YES;
+                           id val = [snapshot value];
+                           XCTAssertTrue([NSNull null] == val, @"First time should be null");
+                           [refs.one setValue:@"test"
+                               withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                                 ready = YES;
+                               }];
+                         }
+                       }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  [refs.one removeObserverWithHandle:handle1];
+
+  ready = NO;
+  [node2 setValue:@"hello"
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        [refs.one
+            observeSingleEventOfType:FIRDataEventTypeValue
+                           withBlock:^(FIRDataSnapshot *snapshot) {
+                             NSString *val = [snapshot value];
+                             XCTAssertTrue([val isEqualToString:@"test"],
+                                           @"Get back the value we set above");
+                             [refs.two
+                                 observeSingleEventOfType:FIRDataEventTypeValue
+                                                withBlock:^(FIRDataSnapshot *snapshot) {
+                                                  NSString *val = [snapshot value];
+                                                  XCTAssertTrue([val isEqualToString:@"test"],
+                                                                @"Get back the value we set above");
+                                                  ready = YES;
+                                                }];
+                           }];
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  // write {x: 1, y : {t: 2, u: 3}}
+  // Listen at /. Then listen at /x/t
+  // unlisten at /y/t. Off at /. Once at /. Ensure data is still all there.
+  // Once at /y. Ensure data is still all there.
+  refs = [FTestHelpers getRandomNodePairWithoutPersistence];
+
+  ready = NO;
+  __block FIRDatabaseHandle deeplisten = NSNotFound;
+  __block FIRDatabaseHandle slashlisten = NSNotFound;
+  __weak FIRDatabaseReference *refOne = refs.one;
+  [refs.one setValue:@{@"x" : @1, @"y" : @{@"t" : @2, @"u" : @3}}
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        slashlisten = [refOne
+            observeEventType:FIRDataEventTypeValue
+                   withBlock:^(FIRDataSnapshot *snapshot) {
+                     deeplisten = [[refOne child:@"y/t"]
+                         observeEventType:FIRDataEventTypeValue
+                                withBlock:^(FIRDataSnapshot *snapshot) {
+                                  [[refOne child:@"y/t"] removeObserverWithHandle:deeplisten];
+                                  [refOne removeObserverWithHandle:slashlisten];
+                                  ready = YES;
+                                }];
+                   }];
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [[refs.one child:@"x"] setValue:@"test"
+              withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                [refs.one observeSingleEventOfType:FIRDataEventTypeValue
+                                         withBlock:^(FIRDataSnapshot *snapshot) {
+                                           NSDictionary *val = [snapshot value];
+                                           NSDictionary *expected =
+                                               @{@"x" : @"test",
+                                                 @"y" : @{@"t" : @2, @"u" : @3}};
+                                           XCTAssertTrue([val isEqualToDictionary:expected],
+                                                         @"Got the final value");
+                                           ready = YES;
+                                         }];
+              }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testUpdateRaisesCorrectLocalEvents {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block FIRDataSnapshot *snap = nil;
+  [node observeSingleEventOfType:FIRDataEventTypeValue
+                       withBlock:^(FIRDataSnapshot *snapshot) {
+                         snap = snapshot;
+                       }];
+
+  __block BOOL ready = NO;
+  [node setValue:@{@"a" : @1, @"b" : @2, @"c" : @3, @"d" : @4}
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  NSArray *expectations = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"d"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:node
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:node
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"d"],
+    [[FTupleEventTypeString alloc] initWithFirebase:node
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+
+  [et addLookingFor:expectations];
+
+  [et waitForInitialization];
+
+  [node updateChildValues:@{@"a" : @4, @"d" : @1}];
+
+  [et wait];
+}
+
+- (void)testUpdateRaisesCorrectRemoteEvents {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@{@"a" : @1, @"b" : @2, @"c" : @3, @"d" : @4}
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  FEventTester *et = [[FEventTester alloc] initFrom:self];
+  NSArray *expectations = @[
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"d"]
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"a"],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeChildChanged
+                                         withString:@"d"],
+    [[FTupleEventTypeString alloc] initWithFirebase:reader
+                                          withEvent:FIRDataEventTypeValue
+                                         withString:nil]
+  ];
+
+  [et addLookingFor:expectations];
+
+  [et waitForInitialization];
+
+  [writer updateChildValues:@{@"a" : @4, @"d" : @1}];
+
+  [et wait];
+
+  ready = NO;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   NSDictionary *result = [snapshot value];
+                   NSDictionary *expected = @{@"a" : @4, @"b" : @2, @"c" : @3, @"d" : @1};
+                   XCTAssertTrue([result isEqualToDictionary:expected], @"Got expected results");
+                   ready = YES;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testUpdateChangesAreStoredCorrectlyByTheServer {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  [self waitForCompletionOf:writer setValue:@{@"a" : @1, @"b" : @2, @"c" : @3, @"d" : @4}];
+
+  [self waitForCompletionOf:writer updateChildValues:@{@"a" : @42}];
+
+  [self snapWaiter:reader
+         withBlock:^(FIRDataSnapshot *snapshot) {
+           NSDictionary *result = [snapshot value];
+           NSDictionary *expected = @{@"a" : @42, @"b" : @2, @"c" : @3, @"d" : @4};
+           XCTAssertTrue([result isEqualToDictionary:expected], @"Expected updated value");
+         }];
+}
+
+- (void)testUpdateDoesntAffectPriorityLocally {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  __block FIRDataSnapshot *snap = nil;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                snap = snapshot;
+              }];
+
+  [ref setValue:@{@"a" : @1, @"b" : @2, @"c" : @3} andPriority:@"testpri"];
+
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([[snap priority] isEqualToString:@"testpri"], @"Got initial priority");
+  snap = nil;
+
+  [ref updateChildValues:@{@"a" : @4}];
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  XCTAssertTrue([[snap priority] isEqualToString:@"testpri"], @"Got initial priority");
+}
+
+- (void)testUpdateDoesntAffectPriorityRemotely {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@{@"a" : @1, @"b" : @2, @"c" : @3}
+              andPriority:@"testpri"
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [reader observeSingleEventOfType:FIRDataEventTypeValue
+                         withBlock:^(FIRDataSnapshot *snapshot) {
+                           NSString *result = [snapshot priority];
+                           XCTAssertTrue([result isEqualToString:@"testpri"],
+                                         @"Expected initial priority");
+                           ready = YES;
+                         }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [writer updateChildValues:@{@"a" : @4}
+        withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+          ready = YES;
+        }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+  [reader observeSingleEventOfType:FIRDataEventTypeValue
+                         withBlock:^(FIRDataSnapshot *snapshot) {
+                           NSString *result = [snapshot priority];
+                           XCTAssertTrue([result isEqualToString:@"testpri"],
+                                         @"Expected initial priority");
+                           ready = YES;
+                         }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+// TODO: On arm hardware Macs, the following test hangs with the emulator, but passes with a real
+// project.
+- (void)SKIPtestUpdateReplacesChildrenAndIsNotRecursive {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block FIRDataSnapshot *localSnap = nil;
+  __block BOOL ready = NO;
+
+  [writer observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   localSnap = snapshot;
+                 }];
+
+  [writer setValue:@{@"a" : @{@"aa" : @1, @"ab" : @2}}];
+  [writer updateChildValues:@{@"a" : @{@"aa" : @1}}
+        withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+          ready = YES;
+        }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+
+  [reader observeSingleEventOfType:FIRDataEventTypeValue
+                         withBlock:^(FIRDataSnapshot *snapshot) {
+                           NSDictionary *result = [snapshot value];
+                           NSDictionary *expected = @{@"a" : @{@"aa" : @1}};
+                           XCTAssertTrue([result isEqualToDictionary:expected],
+                                         @"Should get new value");
+                           ready = YES;
+                         }];
+
+  [self waitUntil:^BOOL {
+    NSDictionary *result = [localSnap value];
+    NSDictionary *expected = @{@"a" : @{@"aa" : @1}};
+    return ready && [result isEqualToDictionary:expected];
+  }];
+}
+
+- (void)testDeepUpdatesWork {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block FIRDataSnapshot *localSnap = nil;
+  __block BOOL ready = NO;
+
+  [writer observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   localSnap = snapshot;
+                 }];
+
+  [writer setValue:@{@"a" : @{@"aa" : @1, @"ab" : @2}}];
+  [writer updateChildValues:@{
+    @"a/aa" : @10,
+    @".priority" : @3.0,
+    @"a/ab" : @{@".priority" : @2.0, @".value" : @20}
+  }
+        withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+          ready = YES;
+        }];
+
+  NSLog(@"[%@] Starting testDeepUpdatesWork", [NSDate date]);
+  XCTestExpectation *initialSetExpectation = [self expectationWithDescription:@"Initial setValue completes"];
+  XCTestExpectation *updateExpectation = [self expectationWithDescription:@"updateChildValues completes"];
+  XCTestExpectation *readerExpectation = [self expectationWithDescription:@"Reader receives value"];
+  XCTestExpectation *localSnapExpectation = [self expectationWithDescription:@"LocalSnap receives final value"];
+
+  __block BOOL initialSetDone = NO;
+
+  NSDictionary *initialData = @{@"a" : @{@"aa" : @1, @"ab" : @2}};
+  NSDictionary *updateData = @{
+    @"a/aa" : @10,
+    @".priority" : @3.0,
+    @"a/ab" : @{@".priority" : @2.0, @".value" : @20}
+  };
+  NSDictionary *expectedReaderValue = @{@"a" : @{@"aa" : @10, @"ab" : @20}};
+
+  // Observe localSnap for the final state
+  // We need to see two distinct values for localSnap: initial set and after update.
+  __block int localSnapUpdateCount = 0;
+  NSDictionary *expectedInitialLocalValue = @{@"a" : @{@"aa" : @1, @"ab" : @2}};
+  NSDictionary *expectedFinalLocalValue = @{@"a" : @{@"aa" : @10, @"ab" : @20}};
+
+  [writer observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   localSnap = snapshot;
+                   NSDictionary *currentValue = [snapshot value];
+                   NSLog(@"[%@] localSnap received value: %@", [NSDate date], currentValue);
+                   if (!initialSetDone) {
+                     if ([currentValue isEqualToDictionary:expectedInitialLocalValue]) {
+                        NSLog(@"[%@] localSnap matched initial expected value.", [NSDate date]);
+                     }
+                   } else {
+                     if ([currentValue isEqualToDictionary:expectedFinalLocalValue]) {
+                       localSnapUpdateCount++;
+                       NSLog(@"[%@] localSnap matched final expected value (count: %d).", [NSDate date], localSnapUpdateCount);
+                       if (localSnapUpdateCount >= 1) {
+                           NSLog(@"[%@] Fulfilling localSnapExpectation.", [NSDate date]);
+                           [localSnapExpectation fulfill];
+                       }
+                     }
+                   }
+                 }];
+
+  NSLog(@"[%@] Performing initial setValue with: %@", [NSDate date], initialData);
+  [writer setValue:initialData
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        if (error) {
+            NSLog(@"[%@] ERROR during initial setValue: %@", [NSDate date], error);
+        } else {
+            NSLog(@"[%@] Initial setValue completed.", [NSDate date]);
+        }
+        XCTAssertNil(error, @"Error during initial setValue");
+        initialSetDone = YES;
+        [initialSetExpectation fulfill];
+      }];
+
+  NSLog(@"[%@] Waiting for initialSetExpectation.", [NSDate date]);
+  [self waitForExpectations:@[initialSetExpectation] timeout:kFirebaseTestTimeout];
+  NSLog(@"[%@] initialSetExpectation fulfilled.", [NSDate date]);
+
+  NSLog(@"[%@] Performing updateChildValues with: %@", [NSDate date], updateData);
+  [writer updateChildValues:updateData
+        withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+          if (error) {
+            NSLog(@"[%@] ERROR during updateChildValues: %@", [NSDate date], error);
+          } else {
+            NSLog(@"[%@] updateChildValues completed.", [NSDate date]);
+          }
+          XCTAssertNil(error, @"Error during updateChildValues");
+          [updateExpectation fulfill];
+        }];
+
+  NSLog(@"[%@] Setting up reader observeSingleEventOfType.", [NSDate date]);
+  [reader observeSingleEventOfType:FIRDataEventTypeValue
+                         withBlock:^(FIRDataSnapshot *snapshot) {
+                           NSDictionary *result = [snapshot value];
+                           NSLog(@"[%@] Reader received value: %@", [NSDate date], result);
+                           XCTAssertNotNil(result, @"Reader received nil snapshot value.");
+                           XCTAssertFalse([result isKindOfClass:[NSNull class]], @"Reader received NSNull value. Expected: %@", expectedReaderValue);
+                           XCTAssertTrue([result isKindOfClass:[NSDictionary class]], @"Reader did not receive a dictionary. Expected: %@, Got: %@", expectedReaderValue, [result class]);
+
+                           XCTAssertTrue([result isEqualToDictionary:expectedReaderValue],
+                                         @"Reader: Should get new value. Expected %@, got %@", expectedReaderValue, result);
+                           NSLog(@"[%@] Fulfilling readerExpectation.", [NSDate date]);
+                           [readerExpectation fulfill];
+                         }];
+
+  NSLog(@"[%@] Waiting for updateExpectation, readerExpectation, localSnapExpectation.", [NSDate date]);
+  [self waitForExpectations:@[updateExpectation, readerExpectation, localSnapExpectation] timeout:kFirebaseTestTimeout];
+  NSLog(@"[%@] All expectations fulfilled.", [NSDate date]);
+
+  NSDictionary *finalLocalValue = [localSnap value];
+  NSLog(@"[%@] Final localSnap value for assertion: %@", [NSDate date], finalLocalValue);
+  XCTAssertTrue([finalLocalValue isEqualToDictionary:expectedFinalLocalValue],
+                @"Final localSnap value should match expected. Expected %@, got %@", expectedFinalLocalValue, finalLocalValue);
+  NSLog(@"[%@] testDeepUpdatesWork finished.", [NSDate date]);
+}
+
+// Type signature means we don't need a test for updating scalars. They wouldn't compile
+
+- (void)testEmptyUpdateWorks {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  __block BOOL ready = NO;
+  [ref updateChildValues:@{}
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        XCTAssertTrue(error == nil, @"Should not be an error");
+        ready = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+// XXX update stress test
+
+- (void)testUpdateFiresCorrectEventWhenAChildIsDeleted {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block FIRDataSnapshot *localSnap = nil;
+  __block FIRDataSnapshot *remoteSnap = nil;
+
+  [self waitForCompletionOf:writer setValue:@{@"a" : @12, @"b" : @6}];
+  [writer observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   localSnap = snapshot;
+                 }];
+
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   remoteSnap = snapshot;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  localSnap = nil;
+  remoteSnap = nil;
+
+  [writer updateChildValues:@{@"a" : [NSNull null]}];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  NSDictionary *expected = @{@"b" : @6};
+  XCTAssertTrue([[remoteSnap value] isEqualToDictionary:expected], @"Removed child");
+  XCTAssertTrue([[localSnap value] isEqualToDictionary:expected], @"Removed child");
+}
+
+- (void)testUpdateFiresCorrectEventOnNewChildren {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block FIRDataSnapshot *localSnap = nil;
+  __block FIRDataSnapshot *remoteSnap = nil;
+
+  [[writer child:@"a"] observeEventType:FIRDataEventTypeValue
+                              withBlock:^(FIRDataSnapshot *snapshot) {
+                                localSnap = snapshot;
+                              }];
+
+  [[reader child:@"a"] observeEventType:FIRDataEventTypeValue
+                              withBlock:^(FIRDataSnapshot *snapshot) {
+                                remoteSnap = snapshot;
+                              }];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  localSnap = nil;
+  remoteSnap = nil;
+
+  [writer updateChildValues:@{@"a" : @42}];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  XCTAssertTrue([[remoteSnap value] isEqualToNumber:@42], @"Added child");
+  XCTAssertTrue([[localSnap value] isEqualToNumber:@42], @"Added child");
+}
+
+- (void)testUpdateFiresCorrectEventOnDeletedChildren {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block FIRDataSnapshot *localSnap = nil;
+  __block FIRDataSnapshot *remoteSnap = nil;
+  [self waitForCompletionOf:writer setValue:@{@"a" : @12}];
+  [[writer child:@"a"] observeEventType:FIRDataEventTypeValue
+                              withBlock:^(FIRDataSnapshot *snapshot) {
+                                localSnap = snapshot;
+                              }];
+
+  [[reader child:@"a"] observeEventType:FIRDataEventTypeValue
+                              withBlock:^(FIRDataSnapshot *snapshot) {
+                                remoteSnap = snapshot;
+                              }];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  localSnap = nil;
+  remoteSnap = nil;
+
+  [writer updateChildValues:@{@"a" : [NSNull null]}];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  XCTAssertTrue([remoteSnap value] == [NSNull null], @"Removed child");
+  XCTAssertTrue([localSnap value] == [NSNull null], @"Removed child");
+}
+
+- (void)testUpdateFiresCorrectEventOnChangedChildren {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  [self waitForCompletionOf:writer setValue:@{@"a" : @12}];
+
+  __block FIRDataSnapshot *localSnap = nil;
+  __block FIRDataSnapshot *remoteSnap = nil;
+
+  [[writer child:@"a"] observeEventType:FIRDataEventTypeValue
+                              withBlock:^(FIRDataSnapshot *snapshot) {
+                                localSnap = snapshot;
+                              }];
+
+  [[reader child:@"a"] observeEventType:FIRDataEventTypeValue
+                              withBlock:^(FIRDataSnapshot *snapshot) {
+                                remoteSnap = snapshot;
+                              }];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  localSnap = nil;
+  remoteSnap = nil;
+
+  [self waitForCompletionOf:writer updateChildValues:@{@"a" : @11}];
+
+  [self waitUntil:^BOOL {
+    return localSnap != nil && remoteSnap != nil;
+  }];
+
+  XCTAssertTrue([[remoteSnap value] isEqualToNumber:@11], @"Changed child");
+  XCTAssertTrue([[localSnap value] isEqualToNumber:@11], @"Changed child");
+}
+
+- (void)testUpdateOfPriorityWorks {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *reader = refs.one;
+  FIRDatabaseReference *writer = refs.two;
+
+  __block BOOL ready = NO;
+  [writer setValue:@{@"a" : @5, @".priority" : @"pri1"}];
+  [writer updateChildValues:@{
+    @"a" : @6,
+    @".priority" : @"pri2",
+    @"b" : @{@".priority" : @"pri3", @"c" : @10}
+  }
+        withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+          NSLog(@"error? %@", error);
+          ready = YES;
+        }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+
+  ready = NO;
+
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   XCTAssertEqualObjects([[snapshot childSnapshotForPath:@"a"] value], @6,
+                                         @"Should match write values");
+                   XCTAssertTrue([[snapshot priority] isEqualToString:@"pri2"],
+                                 @"Should get updated priority");
+                   XCTAssertTrue(
+                       [[[snapshot childSnapshotForPath:@"b"] priority] isEqualToString:@"pri3"],
+                       @"Should get updated priority");
+                   XCTAssertEqualObjects([[snapshot childSnapshotForPath:@"b/c"] value], @10,
+                                         @"Should match write values");
+                   ready = YES;
+                 }];
+
+  [self waitUntil:^BOOL {
+    return ready;
+  }];
+}
+
+- (void)testSetWithCircularReferenceFails {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  NSMutableDictionary *toSet = [[NSMutableDictionary alloc] init];
+  NSDictionary *lol = @{@"foo" : @"bar", @"circular" : toSet};
+  [toSet setObject:lol forKey:@"lol"];
+
+  XCTAssertThrows([ref setValue:toSet], @"Should not be able to set circular dictionary");
+}
+
+- (void)testLargeNumbers {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+  long long jsMaxInt = 9007199254740992;
+  long jsMaxIntPlusOne = (long)jsMaxInt + 1;
+  NSNumber *toSet = [NSNumber numberWithLong:jsMaxIntPlusOne];
+  [ref setValue:toSet];
+
+  __block FIRDataSnapshot *snap = nil;
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snapshot) {
+                snap = snapshot;
+              }];
+
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  NSNumber *result = [snap value];
+  XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number");
+
+  toSet = [NSNumber numberWithLong:LONG_MAX];
+  snap = nil;
+
+  [ref setValue:toSet];
+
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  result = [snap value];
+  XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number");
+
+  snap = nil;
+  toSet = [NSNumber numberWithDouble:DBL_MAX];
+  [ref setValue:toSet];
+
+  [self waitUntil:^BOOL {
+    return snap != nil;
+  }];
+
+  result = [snap value];
+  XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number");
+}
+
+#ifdef FLAKY_TEST
+- (void)testParentDeleteShadowsChildListeners {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *deleter = refs.two;
+
+  NSString *childName = [writer childByAutoId].key;
+
+  __block BOOL called = NO;
+  [[deleter child:childName]
+      observeEventType:FIRDataEventTypeValue
+             withBlock:^(FIRDataSnapshot *snapshot) {
+               XCTAssertFalse(called, @"Should only be hit once");
+               called = YES;
+               XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null");
+             }];
+
+  WAIT_FOR(called);
+
+  __block BOOL done = NO;
+  [[writer child:childName] setValue:@"foo"];
+  [deleter removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+    done = YES;
+  }];
+
+  WAIT_FOR(done);
+  [deleter removeAllObservers];
+}
+#endif
+
+- (void)testParentDeleteShadowsChildListenersWithNonDefaultQuery {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *deleter = refs.two;
+
+  NSString *childName = [writer childByAutoId].key;
+
+  __block BOOL queryCalled = NO;
+  __block BOOL deepChildCalled = NO;
+  [[[[deleter child:childName] queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"]
+      observeEventType:FIRDataEventTypeValue
+             withBlock:^(FIRDataSnapshot *snapshot) {
+               XCTAssertFalse(queryCalled, @"Should only be hit once");
+               queryCalled = YES;
+               XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null");
+             }];
+
+  [[[deleter child:childName] child:@"a"]
+      observeEventType:FIRDataEventTypeValue
+             withBlock:^(FIRDataSnapshot *snapshot) {
+               XCTAssertFalse(deepChildCalled, @"Should only be hit once");
+               deepChildCalled = YES;
+               XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null");
+             }];
+
+  WAIT_FOR(deepChildCalled && queryCalled);
+
+  __block BOOL done = NO;
+  [[writer child:childName] setValue:@"foo"];
+  [deleter removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+    done = YES;
+  }];
+
+  WAIT_FOR(done);
+}
+
+- (void)testLocalServerTimestampEventuallyButNotImmediatelyMatchServer {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+  __block int done = 0;
+
+  NSMutableArray *readSnaps = [[NSMutableArray alloc] init];
+  NSMutableArray *writeSnaps = [[NSMutableArray alloc] init];
+
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   if ([snapshot value] != [NSNull null]) {
+                     [readSnaps addObject:snapshot];
+                     if (readSnaps.count == 1) {
+                       done += 1;
+                     }
+                   }
+                 }];
+
+  [writer observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   if ([snapshot value] != [NSNull null]) {
+                     [writeSnaps addObject:snapshot];
+                     if (writeSnaps.count == 2) {
+                       done += 1;
+                     }
+                   }
+                 }];
+
+  [writer setValue:[FIRServerValue timestamp] andPriority:[FIRServerValue timestamp]];
+
+  [self waitUntil:^BOOL {
+    return done == 2;
+  }];
+
+  XCTAssertEqual((unsigned long)[readSnaps count], (unsigned long)1,
+                 @"Should have received one snapshot on reader");
+  XCTAssertEqual((unsigned long)[writeSnaps count], (unsigned long)2,
+                 @"Should have received two snapshots on writer");
+
+  FIRDataSnapshot *firstReadSnap = [readSnaps objectAtIndex:0];
+  FIRDataSnapshot *firstWriteSnap = [writeSnaps objectAtIndex:0];
+  FIRDataSnapshot *secondWriteSnap = [writeSnaps objectAtIndex:1];
+
+  NSNumber *now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+  XCTAssertTrue([now doubleValue] - [firstWriteSnap.value doubleValue] < 3000,
+                @"Should have received a local event with a value close to timestamp");
+  XCTAssertTrue([now doubleValue] - [firstWriteSnap.priority doubleValue] < 3000,
+                @"Should have received a local event with a priority close to timestamp");
+  XCTAssertTrue([now doubleValue] - [secondWriteSnap.value doubleValue] < 3000,
+                @"Should have received a server event with a value close to timestamp");
+  XCTAssertTrue([now doubleValue] - [secondWriteSnap.priority doubleValue] < 3000,
+                @"Should have received a server event with a priority close to timestamp");
+
+  XCTAssertFalse([firstWriteSnap value] == [secondWriteSnap value],
+                 @"Initial and future writer values should be different");
+  XCTAssertFalse([firstWriteSnap priority] == [secondWriteSnap priority],
+                 @"Initial and future writer priorities should be different");
+  XCTAssertEqualObjects(firstReadSnap.value, secondWriteSnap.value,
+                        @"Eventual reader and writer values should be equal");
+  XCTAssertEqualObjects(firstReadSnap.priority, secondWriteSnap.priority,
+                        @"Eventual reader and writer priorities should be equal");
+}
+
+- (void)testServerTimestampSetWithPriorityRemoteEvents {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  NSDictionary *data = @{
+    @"a" : [FIRServerValue timestamp],
+    @"b" : @{@".value" : [FIRServerValue timestamp], @".priority" : [FIRServerValue timestamp]}
+  };
+
+  __block BOOL done = NO;
+  [writer setValue:data
+              andPriority:[FIRServerValue timestamp]
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        done = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  [self
+      snapWaiter:reader
+       withBlock:^(FIRDataSnapshot *snapshot) {
+         NSDictionary *value = [snapshot value];
+         NSNumber *now =
+             [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+         NSNumber *timestamp = [snapshot priority];
+         XCTAssertTrue([[snapshot priority] isKindOfClass:[NSNumber class]],
+                       @"Should get back number");
+         XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000,
+                       @"Number should be no more than 2 seconds ago");
+         XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"a"],
+                               @"Should get back matching ServerValue.TIMESTAMP");
+         XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"b"],
+                               @"Should get back matching ServerValue.TIMESTAMP");
+         XCTAssertEqualObjects([snapshot priority], [[snapshot childSnapshotForPath:@"b"] priority],
+                               @"Should get back matching ServerValue.TIMESTAMP");
+       }];
+}
+
+- (void)testServerTimestampSetPriorityRemoteEvents {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block FIRDataSnapshot *snap = nil;
+  [reader observeEventType:FIRDataEventTypeChildMoved
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   snap = snapshot;
+                 }];
+
+  [self waitForCompletionOf:[writer child:@"a"] setValue:@1 andPriority:nil];
+  [self waitForCompletionOf:[writer child:@"b"] setValue:@1 andPriority:@1];
+  [self waitForValueOf:[reader child:@"a"] toBe:@1];
+
+  __block BOOL done = NO;
+  [[writer child:@"a"] setPriority:[FIRServerValue timestamp]
+               withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                 done = YES;
+               }];
+
+  [self waitUntil:^BOOL {
+    return done && snap != nil;
+  }];
+
+  NSNumber *now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+  NSNumber *timestamp = [snap priority];
+  XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number");
+  XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000,
+                @"Number should be no more than 2 seconds ago");
+}
+
+- (void)testServerTimestampUpdateRemoteEvents {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block FIRDataSnapshot *snap = nil;
+  __block BOOL done = NO;
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   snap = snapshot;
+                   if (snap && [[snap childSnapshotForPath:@"a/b/d"] value] != [NSNull null]) {
+                     done = YES;
+                   }
+                 }];
+
+  [[writer child:@"a/b/c"] setValue:@1];
+  [[writer child:@"a"] updateChildValues:@{@"b" : @{@"c" : [FIRServerValue timestamp], @"d" : @1}}];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  NSNumber *now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+  NSNumber *timestamp = [[snap childSnapshotForPath:@"a/b/c"] value];
+  XCTAssertTrue([[[snap childSnapshotForPath:@"a/b/c"] value] isKindOfClass:[NSNumber class]],
+                @"Should get back number");
+  XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000,
+                @"Number should be no more than 2 seconds ago");
+}
+
+- (void)testServerTimestampSetWithPriorityLocalEvents {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  NSDictionary *data = @{
+    @"a" : [FIRServerValue timestamp],
+    @"b" : @{@".value" : [FIRServerValue timestamp], @".priority" : [FIRServerValue timestamp]}
+  };
+
+  __block FIRDataSnapshot *snap = nil;
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 snap = snapshot;
+               }];
+
+  __block BOOL done = NO;
+  [node setValue:data
+              andPriority:[FIRServerValue timestamp]
+      withCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) {
+        done = YES;
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  [self
+      snapWaiter:node
+       withBlock:^(FIRDataSnapshot *snapshot) {
+         NSDictionary *value = [snapshot value];
+         NSNumber *now =
+             [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+         NSNumber *timestamp = [snapshot priority];
+         XCTAssertTrue([[snapshot priority] isKindOfClass:[NSNumber class]],
+                       @"Should get back number");
+         XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000,
+                       @"Number should be no more than 2 seconds ago");
+         XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"a"],
+                               @"Should get back matching ServerValue.TIMESTAMP");
+         XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"b"],
+                               @"Should get back matching ServerValue.TIMESTAMP");
+         XCTAssertEqualObjects([snapshot priority], [[snapshot childSnapshotForPath:@"b"] priority],
+                               @"Should get back matching ServerValue.TIMESTAMP");
+       }];
+}
+
+- (void)testServerTimestampSetPriorityLocalEvents {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block FIRDataSnapshot *snap = nil;
+  [node observeEventType:FIRDataEventTypeChildMoved
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 snap = snapshot;
+               }];
+
+  __block BOOL done = NO;
+
+  [[node child:@"a"] setValue:@1 andPriority:nil];
+  [[node child:@"b"] setValue:@1 andPriority:@1];
+  [[node child:@"a"] setPriority:[FIRServerValue timestamp]
+             withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+               done = YES;
+             }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  NSNumber *now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+  NSNumber *timestamp = [snap priority];
+  XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number");
+  XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000,
+                @"Number should be no more than 2 seconds ago");
+}
+
+- (void)testServerTimestampUpdateLocalEvents {
+  FIRDatabaseReference *node1 = [FTestHelpers getRandomNode];
+
+  __block FIRDataSnapshot *snap1 = nil;
+  [node1 observeEventType:FIRDataEventTypeValue
+                withBlock:^(FIRDataSnapshot *snapshot) {
+                  snap1 = snapshot;
+                }];
+
+  __block FIRDataSnapshot *snap2 = nil;
+  [node1 observeEventType:FIRDataEventTypeValue
+                withBlock:^(FIRDataSnapshot *snapshot) {
+                  snap2 = snapshot;
+                }];
+
+  [node1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+    [currentData setValue:[FIRServerValue timestamp]];
+    return [FIRTransactionResult successWithValue:currentData];
+  }];
+
+  [self waitUntil:^BOOL {
+    return snap1 != nil && snap2 != nil && [snap1 value] != nil && [snap2 value] != nil;
+  }];
+
+  NSNumber *now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+
+  NSNumber *timestamp1 = [snap1 value];
+  XCTAssertTrue([[snap1 value] isKindOfClass:[NSNumber class]], @"Should get back number");
+  XCTAssertTrue([now doubleValue] - [timestamp1 doubleValue] < 2000,
+                @"Number should be no more than 2 seconds ago");
+
+  NSNumber *timestamp2 = [snap2 value];
+  XCTAssertTrue([[snap2 value] isKindOfClass:[NSNumber class]], @"Should get back number");
+  XCTAssertTrue([now doubleValue] - [timestamp2 doubleValue] < 2000,
+                @"Number should be no more than 2 seconds ago");
+}
+
+- (void)testServerTimestampTransactionLocalEvents {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block FIRDataSnapshot *snap = nil;
+  [node observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 snap = snapshot;
+               }];
+
+  [[node child:@"a/b/c"] setValue:@1];
+  [[node child:@"a"] updateChildValues:@{@"b" : @{@"c" : [FIRServerValue timestamp], @"d" : @1}}];
+
+  [self waitUntil:^BOOL {
+    return snap != nil && [[snap childSnapshotForPath:@"a/b/d"] value] != nil;
+  }];
+
+  NSNumber *now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970] * 1000)];
+  NSNumber *timestamp = [[snap childSnapshotForPath:@"a/b/c"] value];
+  XCTAssertTrue([[[snap childSnapshotForPath:@"a/b/c"] value] isKindOfClass:[NSNumber class]],
+                @"Should get back number");
+  XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000,
+                @"Number should be no more than 2 seconds ago");
+}
+
+- (void)testServerIncrementOverwritesExistingDataOnline {
+  [self checkServerIncrementOverwritesExistingDataWhileOnline:true];
+}
+
+- (void)testServerIncrementOverwritesExistingDataOffline {
+  [self checkServerIncrementOverwritesExistingDataWhileOnline:false];
+}
+
+- (void)checkServerIncrementOverwritesExistingDataWhileOnline:(BOOL)online {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  __block NSMutableArray *found = [NSMutableArray new];
+  NSMutableArray *expected = [NSMutableArray new];
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snap) {
+                [found addObject:snap.value];
+              }];
+
+  // Going offline ensures that local events get queued up before server events
+  if (!online) {
+    [ref.repo interrupt];
+  }
+
+  // null + incr
+  [ref setValue:[FIRServerValue increment:@1]];
+  [expected addObject:@1];
+
+  // number + incr
+  [ref setValue:@5];
+  [ref setValue:[FIRServerValue increment:@1]];
+  [expected addObject:@5];
+  [expected addObject:@6];
+
+  // string + incr
+  [ref setValue:@"hello"];
+  [ref setValue:[FIRServerValue increment:@1]];
+  [expected addObject:@"hello"];
+  [expected addObject:@1];
+
+  // object + incr
+  [ref setValue:@{@"hello" : @"world"}];
+  [ref setValue:[FIRServerValue increment:@1]];
+  [expected addObject:@{@"hello" : @"world"}];
+  [expected addObject:@1];
+
+  [self waitUntil:^BOOL {
+    return found.count == expected.count;
+  }];
+  XCTAssertEqualObjects(expected, found);
+
+  if (!online) {
+    [ref.repo resume];
+  }
+}
+
+- (void)testServerIncrementPriorityOnline {
+  [self checkServerIncrementPriorityWhileOnline:true];
+}
+
+- (void)testServerIncrementPriorityOffline {
+  [self checkServerIncrementPriorityWhileOnline:false];
+}
+
+- (void)checkServerIncrementPriorityWhileOnline:(BOOL)online {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  if (!online) {
+    [ref.repo interrupt];
+  }
+  __block NSMutableArray *found = [NSMutableArray new];
+  NSMutableArray *expected = [NSMutableArray new];
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snap) {
+                [found addObject:snap.priority];
+              }];
+
+  // Going offline ensures that local events get queued up before server events
+  // Also necessary because increment may not be live yet in the server.
+  if (!online) {
+    [ref.repo interrupt];
+  }
+
+  // null + incr
+  [ref setValue:@0 andPriority:[FIRServerValue increment:@1]];
+  [expected addObject:@1];
+  [ref setValue:@0 andPriority:[FIRServerValue increment:@1.5]];
+  [expected addObject:@2.5];
+
+  [self waitUntil:^BOOL {
+    return found.count == expected.count;
+  }];
+  XCTAssertEqualObjects(expected, found);
+
+  if (!online) {
+    [ref.repo resume];
+  }
+}
+
+- (void)testServerIncrementOverflowAndTypeCoercion {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  __block NSMutableArray *found = [NSMutableArray new];
+  __block NSMutableArray *foundTypes = [NSMutableArray new];
+  NSMutableArray *expected = [NSMutableArray new];
+  NSMutableArray *expectedTypes = [NSMutableArray new];
+  [ref observeEventType:FIRDataEventTypeValue
+              withBlock:^(FIRDataSnapshot *snap) {
+                [found addObject:snap.value];
+                [foundTypes addObject:@([(NSNumber *)snap.value objCType])];
+              }];
+
+  // Going offline ensures that local events get queued up before server events
+  // Also necessary because increment may not be live yet in the server.
+  [ref.repo interrupt];
+
+  // long + double = double
+  [ref setValue:@1];
+  [ref setValue:[FIRServerValue increment:@1.0]];
+  [expected addObject:@1];
+  [expected addObject:@2.0];
+  [expectedTypes addObject:@(@encode(int))];
+  [expectedTypes addObject:@(@encode(double))];
+
+  // double + long = double
+  [ref setValue:@1.5];
+  [ref setValue:[FIRServerValue increment:@1]];
+  [expected addObject:@1.5];
+  [expected addObject:@2.5];
+  [expectedTypes addObject:@(@encode(double))];
+  [expectedTypes addObject:@(@encode(double))];
+
+  // long overflow = double
+  [ref setValue:@(1)];
+  [ref setValue:[FIRServerValue increment:@(LONG_MAX)]];
+  [expected addObject:@(1)];
+  [expected addObject:@(LONG_MAX + 1.0)];
+  [expectedTypes addObject:@(@encode(int))];
+  [expectedTypes addObject:@(@encode(double))];
+
+  // unsigned long long overflow = double
+  [ref setValue:@1];
+  [ref setValue:[FIRServerValue increment:@((unsigned long long)ULLONG_MAX)]];
+  [expected addObject:@1];
+  [expected addObject:@((double)ULLONG_MAX + 1)];
+  [expectedTypes addObject:@(@encode(int))];
+  [expectedTypes addObject:@(@encode(double))];
+
+  // long underflow = double
+  [ref setValue:@(-1)];
+  [ref setValue:[FIRServerValue increment:@(LONG_MIN)]];
+  [expected addObject:@(-1)];
+  [expected addObject:@(LONG_MIN - 1.0)];
+  [expectedTypes addObject:@(@encode(int))];
+  [expectedTypes addObject:@(@encode(double))];
+
+  [self waitUntil:^BOOL {
+    return found.count == expected.count && foundTypes.count == expectedTypes.count;
+  }];
+  XCTAssertEqualObjects(expectedTypes, foundTypes);
+  XCTAssertEqualObjects(expected, found);
+  [ref.repo resume];
+}
+
+- (void)testUpdateAfterChildSet {
+  FIRDatabaseReference *node = [FTestHelpers getRandomNode];
+
+  __block BOOL done = NO;
+  __weak FIRDatabaseReference *weakRef = node;
+  [node setValue:@{@"a" : @"a"}
+      withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+        [weakRef observeEventType:FIRDataEventTypeValue
+                        withBlock:^(FIRDataSnapshot *snapshot) {
+                          if (snapshot.childrenCount == 3 && [snapshot hasChild:@"a"] &&
+                              [snapshot hasChild:@"b"] && [snapshot hasChild:@"c"]) {
+                            done = YES;
+                          }
+                        }];
+
+        [[weakRef child:@"b"] setValue:@"b"];
+
+        [weakRef updateChildValues:@{@"c" : @"c"}];
+      }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+}
+
+- (void)testDeltaSyncNoDataUpdatesAfterReconnect {
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  FIRDatabaseConfig *cfg = [FTestHelpers configForName:@"test-config"];
+  FIRDatabaseReference *ref2 = [[FTestHelpers databaseForConfig:cfg] referenceWithPath:ref.key];
+  __block id data = @{@"a" : @1, @"b" : @2, @"c" : @{@".priority" : @3, @".value" : @3}, @"d" : @4};
+  [self waitForCompletionOf:ref setValue:data];
+
+  __block BOOL gotData = NO;
+  [ref2 observeEventType:FIRDataEventTypeValue
+               withBlock:^(FIRDataSnapshot *snapshot) {
+                 XCTAssertFalse(gotData, @"event triggered twice.");
+                 gotData = YES;
+                 XCTAssertEqualObjects(snapshot.valueInExportFormat, data, @"Got wrong data.");
+               }];
+
+  [self waitUntil:^BOOL {
+    return gotData;
+  }];
+
+  __block BOOL done = NO;
+  XCTAssertEqual(ref2.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
+
+  // Bounce connection
+  [FRepoManager interrupt:cfg];
+  [FRepoManager resume:cfg];
+
+  [[[ref2 root] child:@".info/connected"]
+      observeEventType:FIRDataEventTypeValue
+             withBlock:^(FIRDataSnapshot *snapshot) {
+               if ([snapshot.value boolValue]) {
+                 // We're connected.  Do one more round-trip to make sure all state restoration is
+                 // done
+                 [[[ref2 root] child:@"foobar/empty/blah"]
+                                setValue:nil
+                     withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                       XCTAssertEqual(ref2.repo.dataUpdateCount, 1L,
+                                      @"Should have gotten one update.");
+                       done = YES;
+                     }];
+               }
+             }];
+
+  [self waitUntil:^BOOL {
+    return done;
+  }];
+
+  // cleanup
+  [FRepoManager interrupt:cfg];
+  [FRepoManager disposeRepos:cfg];
+}
+
+- (void)testServerTimestampEventualConsistencyBetweenLocalAndRemote {
+  FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+  FIRDatabaseReference *writer = refs.one;
+  FIRDatabaseReference *reader = refs.two;
+
+  __block FIRDataSnapshot *writerSnap = nil;
+  __block FIRDataSnapshot *readerSnap = nil;
+
+  [reader observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   readerSnap = snapshot;
+                 }];
+
+  [writer observeEventType:FIRDataEventTypeValue
+                 withBlock:^(FIRDataSnapshot *snapshot) {
+                   writerSnap = snapshot;
+                 }];
+
+  [writer setValue:[FIRServerValue timestamp] andPriority:[FIRServerValue timestamp]];
+
+  [self waitUntil:^BOOL {
+    if (readerSnap && writerSnap && [[readerSnap value] isKindOfClass:[NSNumber class]] &&
+        [[writerSnap value] isKindOfClass:[NSNumber class]]) {
+      if ([[readerSnap value] doubleValue] == [[writerSnap value] doubleValue]) {
+        return YES;
+      }
+    }
+    return NO;
+  }];
+}
+
+// Listens at a location and then creates a bunch of children, waiting for them all to complete.
+- (void)testChildAddedPerf1 {
+  if (!runPerfTests) return;
+
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+  [ref observeEventType:FIRDataEventTypeChildAdded
+              withBlock:^(FIRDataSnapshot *snapshot){
+
+              }];
+
+  NSDate *start = [NSDate date];
+  int COUNT = 1000;
+  __block BOOL done = NO;
+  __block NSDate *finished = nil;
+  for (int i = 0; i < COUNT; i++) {
+    [[ref childByAutoId] setValue:@"01234567890123456789012345678901234567890123456789"
+              withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                if (i == (COUNT - 1)) {
+                  finished = [NSDate date];
+                  done = YES;
+                }
+              }];
+  }
+  [self
+      waitUntil:^BOOL {
+        return done;
+      }
+        timeout:300];
+  NSTimeInterval elapsed = [finished timeIntervalSinceDate:start];
+  NSLog(@"Elapsed: %f", elapsed);
+}
+
+// Listens at a location, then adds a bunch of grandchildren under a single child.
+- (void)testDeepChildAddedPerf1 {
+  if (!runPerfTests) return;
+
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode], *childRef = [ref child:@"child"];
+
+  [ref observeEventType:FIRDataEventTypeChildAdded
+              withBlock:^(FIRDataSnapshot *snapshot){
+
+              }];
+
+  NSDate *start = [NSDate date];
+  int COUNT = 1000;
+  __block BOOL done = NO;
+  __block NSDate *finished = nil;
+  for (int i = 0; i < COUNT; i++) {
+    [[childRef childByAutoId] setValue:@"01234567890123456789012345678901234567890123456789"
+                   withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+                     if (i == (COUNT - 1)) {
+                       finished = [NSDate date];
+                       done = YES;
+                     }
+                   }];
+  }
+  [self
+      waitUntil:^BOOL {
+        return done;
+      }
+        timeout:300];
+
+  NSTimeInterval elapsed = [finished timeIntervalSinceDate:start];
+  NSLog(@"Elapsed: %f", elapsed);
+}
+
+// Listens at a location, then adds a bunch of grandchildren under a single child, but does it with
+// merges. NOTE[2015-07-14]: This test is still pretty slow, because [FWriteTree removeWriteId] ends
+// up rebuilding the tree after every ack.
+- (void)testDeepChildAddedPerfViaMerge1 {
+  if (!runPerfTests) return;
+
+  FIRDatabaseReference *ref = [FTestHelpers getRandomNode], *childRef = [ref child:@"child"];
+
+  [ref observeEventType:FIRDataEventTypeChildAdded
+              withBlock:^(FIRDataSnapshot *snapshot){
+
+              }];
+
+  NSDate *start = [NSDate date];
+  int COUNT = 250;
+  __block BOOL done = NO;
+  __block NSDate *finished = nil;
+  for (int i = 0; i < COUNT; i++) {
+    NSString *childName = [childRef childByAutoId].key;
+    [childRef updateChildValues:@{childName : @"01234567890123456789012345678901234567890123456789"}
+            withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+              if (i == (COUNT - 1)) {
+                finished = [NSDate date];
+                done = YES;
+              }
+            }];
+  }
+  [self
+      waitUntil:^BOOL {
+        return done;
+      }
+        timeout:300];
+
+  NSTimeInterval elapsed = [finished timeIntervalSinceDate:start];
+  NSLog(@"Elapsed: %f", elapsed);
+}
+
+@end

+ 46 - 0
FTestHelpers.h

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+#import "FirebaseDatabase/Sources/Api/Private/FIRDatabaseReference_Private.h"
+#import "FirebaseDatabase/Sources/Core/FRepoManager.h"
+#import "FirebaseDatabase/Sources/Snapshot/FSnapshotUtilities.h"
+#import "FirebaseDatabase/Sources/Utilities/Tuples/FTupleFirebase.h"
+#import "FirebaseDatabase/Tests/Helpers/FTestContants.h"
+
+#define WAIT_FOR(x)   \
+  [self waitUntil:^{  \
+    return (BOOL)(x); \
+  }];
+
+#define NODE(__node) [FSnapshotUtilities nodeFrom:(__node)]
+#define PATH(__path) [FPath pathWithString:(__path)]
+
+@interface FTestHelpers : XCTestCase
++ (NSString *)databaseURL;
++ (FIRDatabaseConfig *)defaultConfig;
++ (FIRDatabaseConfig *)configForName:(NSString *)name;
++ (FIRDatabase *)defaultDatabase;
++ (FIRDatabase *)databaseForConfig:(FIRDatabaseConfig *)config;
++ (FIRDatabaseReference *)getRandomNode;
++ (FIRDatabaseReference *)getRandomNodeWithoutPersistence;
++ (FTupleFirebase *)getRandomNodePair;
++ (FTupleFirebase *)getRandomNodePairWithoutPersistence;
++ (FTupleFirebase *)getRandomNodeTriple;
++ (id<FNode>)leafNodeOfSize:(NSUInteger)size;
+
+@end

+ 175 - 0
FTestHelpers.m

@@ -0,0 +1,175 @@
+/*
+ * Copyright 2017 Google
+ *
+ * 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.
+ */
+
+#import "FirebaseDatabase/Tests/Helpers/FTestHelpers.h"
+
+#import "FirebaseAuth/Interop/Public/FirebaseAuthInterop/FIRAuthInterop.h"
+#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
+
+#import "FirebaseDatabase/Sources/Api/Private/FIRDatabase_Private.h"
+#import "FirebaseDatabase/Sources/Constants/FConstants.h"
+#import "FirebaseDatabase/Sources/FIRDatabaseConfig_Private.h"
+
+#import "SharedTestUtilities/AppCheckFake/FIRAppCheckFake.h"
+#import "SharedTestUtilities/AppCheckFake/FIRAppCheckTokenResultFake.h"
+#import "SharedTestUtilities/FIRAuthInteropFake.h"
+
+@implementation FTestHelpers
+
++ (NSTimeInterval)waitUntil:(BOOL (^)(void))predicate timeout:(NSTimeInterval)seconds {
+  NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
+  NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:seconds];
+  NSTimeInterval timeoutTime = [timeoutDate timeIntervalSinceReferenceDate];
+  NSTimeInterval currentTime;
+
+  for (currentTime = [NSDate timeIntervalSinceReferenceDate];
+       !predicate() && currentTime < timeoutTime;
+       currentTime = [NSDate timeIntervalSinceReferenceDate]) {
+    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
+                             beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
+  }
+
+  NSTimeInterval finish = [NSDate timeIntervalSinceReferenceDate];
+
+  NSAssert(currentTime <= timeoutTime, @"Timed out");
+
+  return (finish - start);
+}
+
++ (FIRDatabaseConfig *)defaultConfig {
+  return [self configForName:@"default"];
+}
+
++ (FIRDatabaseConfig *)configForName:(NSString *)name {
+  id<FIRAuthInterop> auth = [[FIRAuthInteropFake alloc] initWithToken:nil userID:nil error:nil];
+  id<FIRAppCheckInterop> appCheck = [[FIRAppCheckFake alloc] init];
+  id<FIRDatabaseConnectionContextProvider> contextProvider =
+      [FIRDatabaseConnectionContextProvider contextProviderWithAuth:auth appCheck:appCheck];
+  return [[FIRDatabaseConfig alloc] initWithSessionIdentifier:name
+                                                  googleAppID:@"fake-app-id"
+                                              contextProvider:contextProvider];
+}
+
++ (NSString *)databaseURL {
+  FIROptions *options = [FIROptions defaultOptions];
+  if (options && ![options.databaseURL isEqualToString:@"https://abc-xyz-123.firebaseio.com"]) {
+    return options.databaseURL;
+  }
+  // If no custom GoogleServices.plist is provided, we default to the Emulator URL.
+  return @"http://localhost:9000";
+}
+
++ (FIRDatabase *)databaseForConfig:(FIRDatabaseConfig *)config {
+  FParsedUrl *parsedUrl = [FUtilities parseUrl:[FTestHelpers databaseURL]];
+  return [FIRDatabase createDatabaseForTests:parsedUrl.repoInfo config:config];
+}
+
++ (FIRDatabase *)defaultDatabase {
+  static FIRDatabase *database = nil;
+  if (database == nil) {
+    database = [FTestHelpers databaseForConfig:[self defaultConfig]];
+  }
+  return database;
+}
+
++ (NSArray *)getRandomNodes:(int)num persistence:(BOOL)persistence {
+  static dispatch_once_t pred = 0;
+  static NSMutableArray *persistenceRefs = nil;
+  static NSMutableArray *noPersistenceRefs = nil;
+  dispatch_once(&pred, ^{
+    persistenceRefs = [[NSMutableArray alloc] init];
+    noPersistenceRefs = [[NSMutableArray alloc] init];
+    // Uncomment the following line to run tests against a background thread
+    //[Firebase setDispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
+  });
+
+  NSMutableArray *refs = (persistence) ? persistenceRefs : noPersistenceRefs;
+
+  while (num > refs.count) {
+    NSString *sessionIdentifier =
+        [NSString stringWithFormat:@"test-config-%@persistence-%lu", (persistence) ? @"" : @"no-",
+                                   (unsigned long)refs.count];
+    FIRDatabaseConfig *config = [self configForName:sessionIdentifier];
+    config.persistenceEnabled = persistence;
+    FIRDatabaseReference *ref = [[self databaseForConfig:config] reference];
+    [refs addObject:ref];
+  }
+
+  NSMutableArray *results = [[NSMutableArray alloc] init];
+  NSString *name = nil;
+  for (int i = 0; i < num; ++i) {
+    FIRDatabaseReference *ref = [refs objectAtIndex:i];
+    if (!name) {
+      name = [ref childByAutoId].key;
+    }
+    [results addObject:[ref child:name]];
+  }
+  return results;
+}
+
+// Helpers
++ (FIRDatabaseReference *)getRandomNode {
+  NSArray *refs = [self getRandomNodes:1 persistence:YES];
+  return [refs objectAtIndex:0];
+}
+
++ (FIRDatabaseReference *)getRandomNodeWithoutPersistence {
+  NSArray *refs = [self getRandomNodes:1 persistence:NO];
+  return refs[0];
+}
+
++ (FTupleFirebase *)getRandomNodePair {
+  NSArray *refs = [self getRandomNodes:2 persistence:YES];
+
+  FTupleFirebase *tuple = [[FTupleFirebase alloc] init];
+  tuple.one = [refs objectAtIndex:0];
+  tuple.two = [refs objectAtIndex:1];
+
+  return tuple;
+}
+
++ (FTupleFirebase *)getRandomNodePairWithoutPersistence {
+  NSArray *refs = [self getRandomNodes:2 persistence:NO];
+
+  FTupleFirebase *tuple = [[FTupleFirebase alloc] init];
+  tuple.one = refs[0];
+  tuple.two = refs[1];
+
+  return tuple;
+}
+
++ (FTupleFirebase *)getRandomNodeTriple {
+  NSArray *refs = [self getRandomNodes:3 persistence:YES];
+  FTupleFirebase *triple = [[FTupleFirebase alloc] init];
+  triple.one = [refs objectAtIndex:0];
+  triple.two = [refs objectAtIndex:1];
+  triple.three = [refs objectAtIndex:2];
+
+  return triple;
+}
+
++ (id<FNode>)leafNodeOfSize:(NSUInteger)size {
+  NSMutableString *string = [NSMutableString string];
+  NSString *pattern = @"abdefghijklmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+  for (NSUInteger i = 0; i < size - pattern.length; i = i + pattern.length) {
+    [string appendString:pattern];
+  }
+  NSUInteger remainingLength = size - string.length;
+  [string appendString:[pattern substringToIndex:remainingLength]];
+  return [FSnapshotUtilities nodeFrom:string];
+}
+
+@end