aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Database
diff options
context:
space:
mode:
Diffstat (limited to 'Example/Database')
-rw-r--r--Example/Database/App/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Example/Database/App/Base.lproj/Main.storyboard27
-rw-r--r--Example/Database/App/Database-Info.plist49
-rw-r--r--Example/Database/App/FIRAppDelegate.h23
-rw-r--r--Example/Database/App/FIRAppDelegate.m52
-rw-r--r--Example/Database/App/FIRViewController.h21
-rw-r--r--Example/Database/App/FIRViewController.m35
-rw-r--r--Example/Database/App/main.m23
-rw-r--r--Example/Database/Tests/FirebaseTests-Info.plist22
-rw-r--r--Example/Database/Tests/Helpers/FDevice.h36
-rw-r--r--Example/Database/Tests/Helpers/FDevice.m133
-rw-r--r--Example/Database/Tests/Helpers/FEventTester.h37
-rw-r--r--Example/Database/Tests/Helpers/FEventTester.m172
-rw-r--r--Example/Database/Tests/Helpers/FIRFakeApp.h27
-rw-r--r--Example/Database/Tests/Helpers/FIRFakeApp.m48
-rw-r--r--Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h28
-rw-r--r--Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m61
-rw-r--r--Example/Database/Tests/Helpers/FMockStorageEngine.h23
-rw-r--r--Example/Database/Tests/Helpers/FMockStorageEngine.m168
-rw-r--r--Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h23
-rw-r--r--Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m90
-rw-r--r--Example/Database/Tests/Helpers/FTestBase.h38
-rw-r--r--Example/Database/Tests/Helpers/FTestBase.m170
-rw-r--r--Example/Database/Tests/Helpers/FTestCachePolicy.h27
-rw-r--r--Example/Database/Tests/Helpers/FTestCachePolicy.m65
-rw-r--r--Example/Database/Tests/Helpers/FTestClock.h28
-rw-r--r--Example/Database/Tests/Helpers/FTestClock.m33
-rw-r--r--Example/Database/Tests/Helpers/FTestContants.h23
-rw-r--r--Example/Database/Tests/Helpers/FTestExpectations.h32
-rw-r--r--Example/Database/Tests/Helpers/FTestExpectations.m88
-rw-r--r--Example/Database/Tests/Helpers/FTestHelpers.h38
-rw-r--r--Example/Database/Tests/Helpers/FTestHelpers.m132
-rw-r--r--Example/Database/Tests/Helpers/FTupleEventTypeString.h33
-rw-r--r--Example/Database/Tests/Helpers/FTupleEventTypeString.m53
-rw-r--r--Example/Database/Tests/Helpers/SenTest+FWaiter.h26
-rw-r--r--Example/Database/Tests/Helpers/SenTest+FWaiter.m57
-rw-r--r--Example/Database/Tests/Integration/FConnectionTest.m77
-rw-r--r--Example/Database/Tests/Integration/FData.h22
-rw-r--r--Example/Database/Tests/Integration/FData.m2687
-rw-r--r--Example/Database/Tests/Integration/FDotInfo.h21
-rw-r--r--Example/Database/Tests/Integration/FDotInfo.m173
-rw-r--r--Example/Database/Tests/Integration/FEventTests.h24
-rw-r--r--Example/Database/Tests/Integration/FEventTests.m506
-rw-r--r--Example/Database/Tests/Integration/FIRAuthTests.m67
-rw-r--r--Example/Database/Tests/Integration/FIRDatabaseQueryTests.h22
-rw-r--r--Example/Database/Tests/Integration/FIRDatabaseQueryTests.m2780
-rw-r--r--Example/Database/Tests/Integration/FIRDatabaseTests.m375
-rw-r--r--Example/Database/Tests/Integration/FKeepSyncedTest.m230
-rw-r--r--Example/Database/Tests/Integration/FOrder.h22
-rw-r--r--Example/Database/Tests/Integration/FOrder.m646
-rw-r--r--Example/Database/Tests/Integration/FOrderByTests.h22
-rw-r--r--Example/Database/Tests/Integration/FOrderByTests.m671
-rw-r--r--Example/Database/Tests/Integration/FPersist.h22
-rw-r--r--Example/Database/Tests/Integration/FPersist.m489
-rw-r--r--Example/Database/Tests/Integration/FRealtime.h22
-rw-r--r--Example/Database/Tests/Integration/FRealtime.m605
-rw-r--r--Example/Database/Tests/Integration/FTransactionTest.h21
-rw-r--r--Example/Database/Tests/Integration/FTransactionTest.m1382
-rw-r--r--Example/Database/Tests/Unit/FArraySortedDictionaryTest.m485
-rw-r--r--Example/Database/Tests/Unit/FCompoundHashTest.m141
-rw-r--r--Example/Database/Tests/Unit/FCompoundWriteTest.m526
-rw-r--r--Example/Database/Tests/Unit/FIRDataSnapshotTests.h21
-rw-r--r--Example/Database/Tests/Unit/FIRDataSnapshotTests.m449
-rw-r--r--Example/Database/Tests/Unit/FIRMutableDataTests.h21
-rw-r--r--Example/Database/Tests/Unit/FIRMutableDataTests.m113
-rw-r--r--Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m583
-rw-r--r--Example/Database/Tests/Unit/FNodeTests.m174
-rw-r--r--Example/Database/Tests/Unit/FPathTests.h21
-rw-r--r--Example/Database/Tests/Unit/FPathTests.m84
-rw-r--r--Example/Database/Tests/Unit/FPersistenceManagerTest.m106
-rw-r--r--Example/Database/Tests/Unit/FPruneForestTest.m98
-rw-r--r--Example/Database/Tests/Unit/FPruningTest.m293
-rw-r--r--Example/Database/Tests/Unit/FQueryParamsTest.m162
-rw-r--r--Example/Database/Tests/Unit/FRangeMergeTest.m271
-rw-r--r--Example/Database/Tests/Unit/FRepoInfoTest.m44
-rw-r--r--Example/Database/Tests/Unit/FSparseSnapshotTests.h21
-rw-r--r--Example/Database/Tests/Unit/FSparseSnapshotTests.m207
-rw-r--r--Example/Database/Tests/Unit/FSyncPointTests.h21
-rw-r--r--Example/Database/Tests/Unit/FSyncPointTests.m905
-rw-r--r--Example/Database/Tests/Unit/FTrackedQueryManagerTest.m338
-rw-r--r--Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m574
-rw-r--r--Example/Database/Tests/Unit/FUtilitiesTest.m116
-rw-r--r--Example/Database/Tests/en.lproj/InfoPlist.strings2
-rw-r--r--Example/Database/Tests/syncPointSpec.json8203
-rw-r--r--Example/Database/Tests/third_party/Base64.h53
-rw-r--r--Example/Database/Tests/third_party/Base64.m202
86 files changed, 27088 insertions, 0 deletions
diff --git a/Example/Database/App/Base.lproj/LaunchScreen.storyboard b/Example/Database/App/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Example/Database/App/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Database/App/Base.lproj/Main.storyboard b/Example/Database/App/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Example/Database/App/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Example/Database/App/Database-Info.plist b/Example/Database/App/Database-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Example/Database/App/Database-Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Example/Database/App/FIRAppDelegate.h b/Example/Database/App/FIRAppDelegate.h
new file mode 100644
index 0000000..e3fba8f
--- /dev/null
+++ b/Example/Database/App/FIRAppDelegate.h
@@ -0,0 +1,23 @@
+/*
+ * 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 UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Example/Database/App/FIRAppDelegate.m b/Example/Database/App/FIRAppDelegate.m
new file mode 100644
index 0000000..0ecfdea
--- /dev/null
+++ b/Example/Database/App/FIRAppDelegate.m
@@ -0,0 +1,52 @@
+// 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 "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+ // Override point for customization after application launch.
+ return YES;
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application
+{
+ // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+ // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application
+{
+ // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+ // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application
+{
+ // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application
+{
+ // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application
+{
+ // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+}
+
+@end
diff --git a/Example/Database/App/FIRViewController.h b/Example/Database/App/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Example/Database/App/FIRViewController.h
@@ -0,0 +1,21 @@
+/*
+ * 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 UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Example/Database/App/FIRViewController.m b/Example/Database/App/FIRViewController.m
new file mode 100644
index 0000000..901accf
--- /dev/null
+++ b/Example/Database/App/FIRViewController.m
@@ -0,0 +1,35 @@
+// 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 "FIRViewController.h"
+
+@interface FIRViewController ()
+
+@end
+
+@implementation FIRViewController
+
+- (void)viewDidLoad
+{
+ [super viewDidLoad];
+ // Do any additional setup after loading the view, typically from a nib.
+}
+
+- (void)didReceiveMemoryWarning
+{
+ [super didReceiveMemoryWarning];
+ // Dispose of any resources that can be recreated.
+}
+
+@end
diff --git a/Example/Database/App/main.m b/Example/Database/App/main.m
new file mode 100644
index 0000000..03b5c12
--- /dev/null
+++ b/Example/Database/App/main.m
@@ -0,0 +1,23 @@
+// 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 UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char * argv[])
+{
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Example/Database/Tests/FirebaseTests-Info.plist b/Example/Database/Tests/FirebaseTests-Info.plist
new file mode 100644
index 0000000..42887ee
--- /dev/null
+++ b/Example/Database/Tests/FirebaseTests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.firebase.mobile.ios.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Example/Database/Tests/Helpers/FDevice.h b/Example/Database/Tests/Helpers/FDevice.h
new file mode 100644
index 0000000..c32aea0
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FDevice.h
@@ -0,0 +1,36 @@
+/*
+ * 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>
+
+@class FIRDatabaseReference;
+@class SenTest;
+
+@interface FDevice : NSObject
+- (id)initOnline;
+- (id)initOffline;
+- (id)initOnlineWithUrl:(NSString *)firebaseUrl;
+- (id)initOfflineWithUrl:(NSString *)firebaseUrl;
+- (void)goOffline;
+- (void)goOnline;
+- (void)restartOnline;
+- (void)restartOffline;
+- (void)waitForIdleUsingWaiter:(XCTest*)waiter;
+- (void)do:(void (^)(FIRDatabaseReference *))action;
+
+- (void)dispose;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FDevice.m b/Example/Database/Tests/Helpers/FDevice.m
new file mode 100644
index 0000000..f9667df
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FDevice.m
@@ -0,0 +1,133 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FDevice.h"
+#import "FIRDatabaseReference.h"
+#import "FRepoManager.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "SenTest+FWaiter.h"
+#import "FTestHelpers.h"
+
+@interface FDevice() {
+ FIRDatabaseConfig * config;
+ NSString *url;
+ BOOL isOnline;
+ BOOL disposed;
+}
+@end
+
+@implementation FDevice
+
+- (id)initOnline {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ return [self initOnlineWithUrl:[ref description]];
+}
+
+- (id)initOffline {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ return [self initOfflineWithUrl:[ref description]];
+}
+
+- (id)initOnlineWithUrl:(NSString *)firebaseUrl {
+ return [self initWithUrl:firebaseUrl andOnline:YES];
+}
+
+- (id)initOfflineWithUrl:(NSString *)firebaseUrl {
+ return [self initWithUrl:firebaseUrl andOnline:NO];
+}
+
+static NSUInteger deviceId = 0;
+
+- (id)initWithUrl:(NSString *)firebaseUrl andOnline:(BOOL)online {
+ self = [super init];
+ if (self) {
+ config = [FIRDatabaseConfig configForName:[NSString stringWithFormat:@"device-%lu", deviceId++]];
+ config.persistenceEnabled = YES;
+ url = firebaseUrl;
+ isOnline = online;
+ }
+ return self;
+}
+
+- (void) dealloc
+{
+ if (!self->disposed) {
+ [NSException raise:NSInternalInconsistencyException format:@"Forgot to dispose device"];
+ }
+}
+
+- (void) dispose {
+ // TODO: clear persistence
+ [FRepoManager disposeRepos:self->config];
+ self->disposed = YES;
+}
+
+- (void)goOffline {
+ isOnline = NO;
+ [FRepoManager interrupt:config];
+}
+
+- (void)goOnline {
+ isOnline = YES;
+ [FRepoManager resume:config];
+}
+
+- (void)restartOnline {
+ @autoreleasepool {
+ [FRepoManager disposeRepos:config];
+ isOnline = YES;
+ }
+}
+
+- (void)restartOffline {
+ @autoreleasepool {
+ [FRepoManager disposeRepos:config];
+ isOnline = NO;
+ }
+}
+
+// Waits for us to connect and then does an extra round-trip to make sure all initial state restoration is completely done.
+- (void)waitForIdleUsingWaiter:(XCTest*)waiter {
+ [self do:^(FIRDatabaseReference *ref) {
+ __block BOOL connected = NO;
+ FIRDatabaseHandle handle = [[ref.root child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ connected = [snapshot.value boolValue];
+ }];
+ [waiter waitUntil:^BOOL { return connected; }];
+ [ref.root removeObserverWithHandle:handle];
+
+ // HACK: Do a deep setPriority (which we expect to fail because there's no data there) to do a no-op roundtrip.
+ __block BOOL done = NO;
+ [[ref.root child:@"ENTOHTNUHOE/ONTEHNUHTOE"] setPriority:@"blah" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ [waiter waitUntil:^BOOL { return done; }];
+ }];
+}
+
+- (void)do:(void (^)(FIRDatabaseReference *))action {
+ @autoreleasepool {
+ FIRDatabaseReference *ref = [[[[FIRDatabaseReference alloc] initWithConfig:self->config] database] referenceFromURL:self->url];
+ if (!isOnline) {
+ [FRepoManager interrupt:config];
+ }
+ action(ref);
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FEventTester.h b/Example/Database/Tests/Helpers/FEventTester.h
new file mode 100644
index 0000000..b3503b9
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FEventTester.h
@@ -0,0 +1,37 @@
+/*
+ * 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>
+
+@interface FEventTester : XCTestCase
+
+- (id)initFrom:(XCTestCase *)elsewhere;
+- (void) addLookingFor:(NSArray *)l;
+- (void) wait;
+- (void) waitForInitialization;
+- (void) unregister;
+
+@property (nonatomic, strong) NSMutableArray* lookingFor;
+@property (readwrite) int callbacksCalled;
+@property (nonatomic, strong) NSMutableDictionary* seenFirebaseLocations;
+//@property (nonatomic, strong) NSMutableDictionary* initializationEvents;
+@property (nonatomic, strong) XCTestCase* from;
+@property (nonatomic, strong) NSMutableArray* errors;
+@property (nonatomic, strong) NSMutableArray* actualPathsAndEvents;
+@property (nonatomic) int initializationEvents;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FEventTester.m b/Example/Database/Tests/Helpers/FEventTester.m
new file mode 100644
index 0000000..fa7c081
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FEventTester.m
@@ -0,0 +1,172 @@
+/*
+ * 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 "FEventTester.h"
+#import "FIRDatabaseReference.h"
+#import "FTupleBoolBlock.h"
+#import "FTupleEventTypeString.h"
+#import "FTestHelpers.h"
+#import "SenTest+FWaiter.h"
+
+@implementation FEventTester
+
+@synthesize lookingFor;
+@synthesize callbacksCalled;
+@synthesize from;
+@synthesize errors;
+@synthesize seenFirebaseLocations;
+@synthesize initializationEvents;
+@synthesize actualPathsAndEvents;
+
+- (id)initFrom:(XCTestCase *)elsewhere
+{
+ self = [super init];
+ if (self) {
+ self.seenFirebaseLocations = [[NSMutableDictionary alloc] init];
+ self.initializationEvents = 0;
+ self.lookingFor = [[NSMutableArray alloc] init];
+ self.actualPathsAndEvents = [[NSMutableArray alloc] init];
+ self.from = elsewhere;
+ self.callbacksCalled = 0;
+ }
+ return self;
+}
+
+- (void) addLookingFor:(NSArray *)l {
+
+ // expect them in the order they're given to us
+ [self.lookingFor addObjectsFromArray:l];
+
+
+ // see notes on ordering of listens in init.spec.js
+ NSArray* toListen = [l sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
+ FTupleEventTypeString* a = obj1;
+ FTupleEventTypeString* b = obj2;
+ NSUInteger lenA = [a.firebase description].length;
+ NSUInteger lenB = [b.firebase description].length;
+ if (lenA < lenB) {
+ return NSOrderedAscending;
+ } else if (lenA == lenB) {
+ return NSOrderedSame;
+ } else {
+ return NSOrderedDescending;
+ }
+ }];
+
+ for(FTupleEventTypeString* fevts in toListen) {
+ if(! [self.seenFirebaseLocations objectForKey:[fevts.firebase description]]) {
+ fevts.vvcallback = [self listenOnPath:fevts.firebase];
+ fevts.initialized = NO;
+ [self.seenFirebaseLocations setObject:fevts forKey:[fevts.firebase description]];
+ }
+ }
+}
+
+- (void) unregister {
+ for(FTupleEventTypeString* fevts in self.lookingFor) {
+ if (fevts.vvcallback) {
+ fevts.vvcallback();
+ }
+ }
+ [self.lookingFor removeAllObjects];
+}
+
+- (fbt_void_void) listenOnPath:(FIRDatabaseReference *)path {
+ FIRDatabaseHandle removedHandle = [path observeEventType:FIRDataEventTypeChildRemoved withBlock:[self makeEventCallback:FIRDataEventTypeChildRemoved]];
+ FIRDatabaseHandle addedHandle = [path observeEventType:FIRDataEventTypeChildAdded withBlock:[self makeEventCallback:FIRDataEventTypeChildAdded]];
+ FIRDatabaseHandle movedHandle = [path observeEventType:FIRDataEventTypeChildMoved withBlock:[self makeEventCallback:FIRDataEventTypeChildMoved]];
+ FIRDatabaseHandle changedHandle = [path observeEventType:FIRDataEventTypeChildChanged withBlock:[self makeEventCallback:FIRDataEventTypeChildChanged]];
+ FIRDatabaseHandle valueHandle = [path observeEventType:FIRDataEventTypeValue withBlock:[self makeEventCallback:FIRDataEventTypeValue]];
+
+ fbt_void_void cb = ^() {
+ [path removeObserverWithHandle:removedHandle];
+ [path removeObserverWithHandle:addedHandle];
+ [path removeObserverWithHandle:movedHandle];
+ [path removeObserverWithHandle:changedHandle];
+ [path removeObserverWithHandle:valueHandle];
+ };
+ return [cb copy];
+}
+
+- (void) wait {
+ [self waitUntil:^BOOL{
+ return self.actualPathsAndEvents.count >= self.lookingFor.count;
+ } timeout:kFirebaseTestTimeout];
+
+ for (int i = 0; i < self.lookingFor.count; ++i) {
+ FTupleEventTypeString* target = [self.lookingFor objectAtIndex:i];
+ FTupleEventTypeString* recvd = [self.actualPathsAndEvents objectAtIndex:i];
+ XCTAssertTrue([target isEqualTo:recvd], @"Expected %@ to match %@", target, recvd);
+ }
+
+ if (self.actualPathsAndEvents.count > self.lookingFor.count) {
+ NSLog(@"Too many events: %@", self.actualPathsAndEvents);
+ XCTFail(@"Received too many events");
+ }
+}
+
+- (void) waitForInitialization {
+ [self waitUntil:^BOOL{
+ for (FTupleEventTypeString* evt in [self.seenFirebaseLocations allValues]) {
+ if (!evt.initialized) {
+ return NO;
+ }
+ }
+
+ // splice out all of the initialization events
+ NSRange theRange;
+ theRange.location = 0;
+ theRange.length = self.initializationEvents;
+ [actualPathsAndEvents removeObjectsInRange:theRange];
+
+ return YES;
+ } timeout:kFirebaseTestTimeout];
+}
+
+- (fbt_void_datasnapshot) makeEventCallback:(FIRDataEventType)type {
+
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot * snap) {
+
+ FIRDatabaseReference * ref = snap.ref;
+ NSString* name = nil;
+ if (type != FIRDataEventTypeValue) {
+ ref = ref.parent;
+ name = snap.key;
+ }
+
+ FTupleEventTypeString* evt = [[FTupleEventTypeString alloc] initWithFirebase:ref withEvent:type withString:name];
+ [actualPathsAndEvents addObject:evt];
+
+ NSLog(@"Adding event: %@ (%@)", evt, [snap value]);
+
+ FTupleEventTypeString* targetEvt = [self.seenFirebaseLocations objectForKey:[ref description]];
+ if (targetEvt && !targetEvt.initialized) {
+ self.initializationEvents++;
+ if (type == FIRDataEventTypeValue) {
+ targetEvt.initialized = YES;
+ }
+ }
+ };
+ return [cb copy];
+}
+
+
+- (void) failWithException:(NSException *) anException {
+ //TODO: FIX
+ @throw anException;
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FIRFakeApp.h b/Example/Database/Tests/Helpers/FIRFakeApp.h
new file mode 100644
index 0000000..afe976a
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRFakeApp.h
@@ -0,0 +1,27 @@
+/*
+ * 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>
+
+@class FIRFakeOptions;
+
+@interface FIRFakeApp : NSObject
+
+- (instancetype) initWithName:(NSString *)name URL:(NSString *)url;
+
+@property(nonatomic, readonly) FIRFakeOptions *options;
+@property(nonatomic, copy, readonly) NSString *name;
+@end
diff --git a/Example/Database/Tests/Helpers/FIRFakeApp.m b/Example/Database/Tests/Helpers/FIRFakeApp.m
new file mode 100644
index 0000000..b7abe81
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRFakeApp.m
@@ -0,0 +1,48 @@
+/*
+ * 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 "FIRFakeApp.h"
+
+@interface FIRFakeOptions: NSObject
+@property(nonatomic, readonly, copy) NSString *databaseURL;
+- (instancetype) initWithURL:(NSString *)url;
+@end
+
+@implementation FIRFakeOptions
+- (instancetype) initWithURL:(NSString *)url {
+ self = [super init];
+ if (self) {
+ self->_databaseURL = url;
+ }
+ return self;
+}
+@end
+
+@implementation FIRFakeApp
+
+- (instancetype) initWithName:(NSString *)name URL:(NSString *)url {
+ self = [super init];
+ if (self) {
+ self->_name = name;
+ self->_options = [[FIRFakeOptions alloc] initWithURL:url];
+ }
+ return self;
+}
+
+- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(void (^)(NSString *_Nullable token, NSError *_Nullable error))callback {
+ callback(nil, nil);
+}
+@end
diff --git a/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h
new file mode 100644
index 0000000..e2a5751
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "FAuthTokenProvider.h"
+
+@interface FIRTestAuthTokenProvider : NSObject <FAuthTokenProvider>
+
+@property (nonatomic, strong) NSString *token;
+@property (nonatomic, strong) NSString *nextToken;
+
+- (instancetype) initWithToken:(NSString *)token;
+- (instancetype) init NS_UNAVAILABLE;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m
new file mode 100644
index 0000000..4719295
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m
@@ -0,0 +1,61 @@
+/*
+ * 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 "FIRTestAuthTokenProvider.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FIRTestAuthTokenProvider ()
+
+@property (nonatomic, strong) NSMutableArray *listeners;
+
+@end
+
+@implementation FIRTestAuthTokenProvider
+
+- (instancetype) initWithToken:(NSString *)token {
+ self = [super init];
+ if (self != nil) {
+ self.listeners = [NSMutableArray array];
+ self.token = token;
+ }
+ return self;
+}
+
+- (void) setToken:(NSString *)token {
+ self->_token = token;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ [self.listeners enumerateObjectsUsingBlock:^(fbt_void_nsstring _Nonnull listener, NSUInteger idx, BOOL * _Nonnull stop) {
+ listener(token);
+ }];
+ });
+
+}
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ if (forceRefresh) {
+ self.token = self.nextToken;
+ }
+ // Simulate delay
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), [FIRDatabaseQuery sharedQueue], ^{
+ callback(self.token, nil);
+ });
+}
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener {
+ [self.listeners addObject:[listener copy]];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FMockStorageEngine.h b/Example/Database/Tests/Helpers/FMockStorageEngine.h
new file mode 100644
index 0000000..98a7d84
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FMockStorageEngine.h
@@ -0,0 +1,23 @@
+/*
+ * 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 "FStorageEngine.h"
+
+@interface FMockStorageEngine : NSObject<FStorageEngine>
+
+@end
diff --git a/Example/Database/Tests/Helpers/FMockStorageEngine.m b/Example/Database/Tests/Helpers/FMockStorageEngine.m
new file mode 100644
index 0000000..98cb596
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FMockStorageEngine.m
@@ -0,0 +1,168 @@
+/*
+ * 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 "FMockStorageEngine.h"
+
+#import "FWriteRecord.h"
+#import "FCompoundWrite.h"
+#import "FNode.h"
+#import "FEmptyNode.h"
+#import "FTrackedQuery.h"
+#import "FPruneForest.h"
+#import "FCompoundWrite.h"
+
+@interface FMockStorageEngine ()
+
+@property (nonatomic) BOOL closed;
+@property (nonatomic, strong) NSMutableDictionary *userWritesDict;
+@property (nonatomic, strong) FCompoundWrite *serverCache;
+@property (nonatomic, strong) NSMutableDictionary *trackedQueries;
+@property (nonatomic, strong) NSMutableDictionary *trackedQueryKeys;
+
+@end
+
+@implementation FMockStorageEngine
+
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ self->_userWritesDict = [NSMutableDictionary dictionary];
+ self->_serverCache = [FCompoundWrite emptyWrite];
+ self->_trackedQueries = [NSMutableDictionary dictionary];
+ self->_trackedQueryKeys = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+- (void)close {
+ self.closed = YES;
+}
+
+- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ FWriteRecord *writeRecord = [[FWriteRecord alloc] initWithPath:path overwrite:node writeId:writeId visible:YES];
+ self.userWritesDict[@(writeId)] = writeRecord;
+}
+
+- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId {
+ FWriteRecord *writeRecord = [[FWriteRecord alloc] initWithPath:path merge:merge writeId:writeId];
+ self.userWritesDict[@(writeId)] = writeRecord;
+}
+
+- (void)removeUserWrite:(NSUInteger)writeId {
+ [self.userWritesDict removeObjectForKey:@(writeId)];
+}
+
+- (void)removeAllUserWrites {
+ [self.userWritesDict removeAllObjects];
+}
+
+- (NSArray *)userWrites {
+ return [[self.userWritesDict allValues] sortedArrayUsingComparator:^NSComparisonResult(FWriteRecord *obj1, FWriteRecord *obj2) {
+ if (obj1.writeId < obj2.writeId) {
+ return NSOrderedAscending;
+ } else if (obj1.writeId > obj2.writeId) {
+ return NSOrderedDescending;
+ } else {
+ return NSOrderedSame;
+ }
+ }];
+}
+
+- (id<FNode>)serverCacheAtPath:(FPath *)path {
+ return [[self.serverCache childCompoundWriteAtPath:path] applyToNode:[FEmptyNode emptyNode]];
+}
+
+- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path {
+ __block id<FNode> children = [FEmptyNode emptyNode];
+ id<FNode> fullNode = [[self.serverCache childCompoundWriteAtPath:path] applyToNode:[FEmptyNode emptyNode]];
+ [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) {
+ children = [children updateImmediateChild:key withNewChild:[fullNode getImmediateChild:key]];
+ }];
+ return children;
+}
+
+- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge {
+ if (merge) {
+ [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> childNode, BOOL *stop) {
+ self.serverCache = [self.serverCache addWrite:childNode atPath:[path childFromString:key]];
+ }];
+ } else {
+ self.serverCache = [self.serverCache addWrite:node atPath:path];
+ }
+}
+
+- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path {
+ self.serverCache = [self.serverCache addCompoundWrite:merge atPath:path];
+}
+
+- (NSUInteger)serverCacheEstimatedSizeInBytes {
+ id data = [[self.serverCache applyToNode:[FEmptyNode emptyNode]] valForExport:YES];
+ return [NSJSONSerialization dataWithJSONObject:data options:0 error:nil].length;
+}
+
+- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)prunePath {
+ [self.serverCache enumerateWrites:^(FPath *absolutePath, id<FNode> node, BOOL *stop) {
+ NSAssert([prunePath isEqual:absolutePath] || ![absolutePath contains:prunePath], @"Pruning at %@ but we found data higher up!", prunePath);
+ if ([prunePath contains:absolutePath]) {
+ FPath *relativePath = [FPath relativePathFrom:prunePath to:absolutePath];
+ if ([pruneForest shouldPruneUnkeptDescendantsAtPath:relativePath]) {
+ __block FCompoundWrite *newCache = [FCompoundWrite emptyWrite];
+ [[pruneForest childAtPath:relativePath] enumarateKeptNodesUsingBlock:^(FPath *keepPath) {
+ newCache = [newCache addWrite:[node getChild:keepPath] atPath:keepPath];
+ }];
+ self.serverCache = [[self.serverCache removeWriteAtPath:absolutePath] addCompoundWrite:newCache atPath:absolutePath];
+ } else {
+ // NOTE: This is technically a valid scenario (e.g. you ask to prune at / but only want to prune
+ // 'foo' and 'bar' and ignore everything else). But currently our pruning will explicitly
+ // prune or keep everything we know about, so if we hit this it means our tracked queries and
+ // the server cache are out of sync.
+ NSAssert([pruneForest shouldKeepPath:relativePath], @"We have data at %@ that is neither pruned nor kept.", relativePath);
+ }
+ }
+ }];
+}
+
+- (NSArray *)loadTrackedQueries {
+ return self.trackedQueries.allValues;
+}
+
+- (void)removeTrackedQuery:(NSUInteger)queryId {
+ [self.trackedQueries removeObjectForKey:@(queryId)];
+ [self.trackedQueryKeys removeObjectForKey:@(queryId)];
+}
+
+- (void)saveTrackedQuery:(FTrackedQuery *)query {
+ self.trackedQueries[@(query.queryId)] = query;
+}
+
+- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId {
+ self.trackedQueryKeys[@(queryId)] = keys;
+}
+
+- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId {
+ NSSet *oldKeys = [self trackedQueryKeysForQuery:queryId];
+ NSMutableSet *newKeys = [NSMutableSet setWithSet:oldKeys];
+ [newKeys minusSet:removed];
+ [newKeys unionSet:added];
+ self.trackedQueryKeys[@(queryId)] = newKeys;
+}
+
+- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId {
+ NSSet *keys = self.trackedQueryKeys[@(queryId)];
+ return keys != nil ? keys : [NSSet set];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h
new file mode 100644
index 0000000..d6d9fd3
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h
@@ -0,0 +1,23 @@
+/*
+ * 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>
+
+@interface FTestAuthTokenGenerator : NSObject
+
++ (NSString *) tokenWithSecret:(NSString *)secret authData:(NSDictionary *)data andOptions:(NSDictionary *)options;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m
new file mode 100644
index 0000000..bd98e82
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m
@@ -0,0 +1,90 @@
+/*
+ * 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 <CommonCrypto/CommonHMAC.h>
+#import "FTestAuthTokenGenerator.h"
+#import "Base64.h"
+
+@implementation FTestAuthTokenGenerator
+
++ (NSString *) jsonStringForData:(id)data {
+ NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data
+ options:kNilOptions error:nil];
+
+ return [[NSString alloc] initWithData:jsonData
+ encoding:NSUTF8StringEncoding];
+}
+
++ (NSNumber *) tokenVersion {
+ return @0;
+}
+
++ (NSMutableDictionary *) createOptionsClaims:(NSDictionary *)options {
+ NSMutableDictionary* claims = [[NSMutableDictionary alloc] init];
+ if (options) {
+ NSDictionary* map = @{
+ @"expires": @"exp",
+ @"notBefore": @"nbf",
+ @"admin": @"admin",
+ @"debug": @"debug",
+ @"simulate": @"simulate"
+ };
+
+ for (NSString* claim in map) {
+ if (options[claim] != nil) {
+ NSString* claimName = [map objectForKey:claim];
+ id val = [options objectForKey:claim];
+ [claims setObject:val forKey:claimName];
+ }
+ }
+ }
+ return claims;
+}
+
++ (NSString *) webSafeBase64:(NSString *)encoded {
+ return [[[encoded stringByReplacingOccurrencesOfString:@"=" withString:@""] stringByReplacingOccurrencesOfString:@"+" withString:@"-"] stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
+}
+
++ (NSString *) base64EncodeString:(NSString *)target {
+ return [self webSafeBase64:[target base64EncodedString]];
+}
+
++ (NSString *) tokenWithClaims:(NSDictionary *)claims andSecret:(NSString *)secret {
+ NSDictionary* headerData = @{@"typ": @"JWT", @"alg": @"HS256"};
+ NSString* encodedHeader = [self base64EncodeString:[self jsonStringForData:headerData]];
+ NSString* encodedClaims = [self base64EncodeString:[self jsonStringForData:claims]];
+
+ NSString* secureBits = [NSString stringWithFormat:@"%@.%@", encodedHeader, encodedClaims];
+
+ const char *cKey = [secret cStringUsingEncoding:NSUTF8StringEncoding];
+ const char *cData = [secureBits cStringUsingEncoding:NSUTF8StringEncoding];
+ unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
+ CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), cHMAC);
+ NSData* hmac = [NSData dataWithBytesNoCopy:cHMAC length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO];
+ NSString* encodedHmac = [self webSafeBase64:[hmac base64EncodedString]];
+ return [NSString stringWithFormat:@"%@.%@.%@", encodedHeader, encodedClaims, encodedHmac];
+}
+
++ (NSString *) tokenWithSecret:(NSString *)secret authData:(NSDictionary *)data andOptions:(NSDictionary *)options {
+ NSMutableDictionary* claims = [self createOptionsClaims:options];
+ [claims setObject:[self tokenVersion] forKey:@"v"];
+ NSNumber* now = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]];
+ [claims setObject:now forKey:@"iat"];
+ [claims setObject:data forKey:@"d"];
+ return [self tokenWithClaims:claims andSecret:secret];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestBase.h b/Example/Database/Tests/Helpers/FTestBase.h
new file mode 100644
index 0000000..8137b94
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestBase.h
@@ -0,0 +1,38 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FTestHelpers.h"
+#import "SenTest+FWaiter.h"
+
+@interface FTestBase : XCTestCase {
+ BOOL runPerfTests;
+}
+
+- (void)snapWaiter:(FIRDatabaseReference *)path withBlock:(fbt_void_datasnapshot)fn;
+- (void)waitUntilConnected:(FIRDatabaseReference *)ref;
+- (void)waitForQueue:(FIRDatabaseReference *)ref;
+- (void)waitForEvents:(FIRDatabaseReference *)ref;
+- (void)waitForRoundTrip:(FIRDatabaseReference *)ref;
+- (void)waitForValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected;
+- (void)waitForExportValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected;
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value;
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value andPriority:(id)priority;
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref updateChildValues:(NSDictionary *)values;
+
+@property(nonatomic, readonly) NSString *databaseURL;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestBase.m b/Example/Database/Tests/Helpers/FTestBase.m
new file mode 100644
index 0000000..f55c73b
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestBase.m
@@ -0,0 +1,170 @@
+/*
+ * 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 "FIRApp.h"
+#import "FIROptions.h"
+#import "FTestBase.h"
+#import "FTestAuthTokenGenerator.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRTestAuthTokenProvider.h"
+
+@implementation FTestBase
+
++ (void)setUp
+{
+ static dispatch_once_t once;
+ dispatch_once(&once, ^ {
+ [FIRApp configure];
+ });
+}
+
+- (void)setUp
+{
+ [super setUp];
+
+ [FIRDatabase setLoggingEnabled:YES];
+ _databaseURL = [[FIRApp defaultApp] options].databaseURL;
+
+ // Disabled normally since they slow down the tests and don't actually assert anything (they just NSLog timings).
+ runPerfTests = NO;
+}
+
+- (void)snapWaiter:(FIRDatabaseReference *)path withBlock:(fbt_void_datasnapshot)fn {
+ __block BOOL done = NO;
+
+ [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snap) {
+ fn(snap);
+ done = YES;
+ }];
+
+ NSTimeInterval timeTaken = [self waitUntil:^BOOL{
+ return done;
+ } timeout:kFirebaseTestWaitUntilTimeout];
+
+ NSLog(@"snapWaiter:withBlock: timeTaken:%f", timeTaken);
+
+ XCTAssertTrue(done, @"Properly finished.");
+}
+
+- (void) waitUntilConnected:(FIRDatabaseReference *)ref {
+ __block BOOL connected = NO;
+ FIRDatabaseHandle handle = [[ref.root child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ connected = [snapshot.value boolValue];
+ }];
+ WAIT_FOR(connected);
+ [ref.root removeObserverWithHandle:handle];
+}
+
+- (void) waitForRoundTrip:(FIRDatabaseReference *)ref {
+ // HACK: Do a deep setPriority (which we expect to fail because there's no data there) to do a no-op roundtrip.
+ __block BOOL done = NO;
+ [[ref.root child:@"ENTOHTNUHOE/ONTEHNUHTOE"] setPriority:@"blah" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) waitForQueue:(FIRDatabaseReference *)ref {
+ dispatch_sync([FIRDatabaseQuery sharedQueue], ^{});
+}
+
+- (void) waitForEvents:(FIRDatabaseReference *)ref {
+ [self waitForQueue:ref];
+ __block BOOL done = NO;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ done = YES;
+ });
+ WAIT_FOR(done);
+}
+
+- (void)waitForValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected {
+ __block id value;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ value = snapshot.value;
+ }];
+
+ @try {
+ [self waitUntil:^BOOL {
+ return [value isEqual:expected];
+ }];
+ } @catch (NSException *exception) {
+ @throw [NSException exceptionWithName:@"DidNotGetValue" reason:@"Did not get expected value"
+ userInfo:@{ @"expected": (!expected ? @"nil" : expected),
+ @"actual": (!value ? @"nil" : value) }];
+ } @finally {
+ [ref removeObserverWithHandle:handle];
+ }
+}
+
+- (void)waitForExportValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected {
+ __block id value;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ value = snapshot.valueInExportFormat;
+ }];
+
+ @try {
+ [self waitUntil:^BOOL {
+ return [value isEqual:expected];
+ }];
+ } @catch (NSException *exception) {
+ if ([exception.name isEqualToString:@"Timed out"]) {
+ @throw [NSException exceptionWithName:@"DidNotGetValue" reason:@"Did not get expected value"
+ userInfo:@{ @"expected": (!expected ? @"nil" : expected),
+ @"actual": (!value ? @"nil" : value) }]; } else {
+ @throw exception;
+ }
+ } @finally {
+ [ref removeObserverWithHandle:handle];
+ }
+}
+
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value {
+ [self waitForCompletionOf:ref setValue:value andPriority:nil];
+}
+
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value andPriority:(id)priority {
+ __block BOOL done = NO;
+ [ref setValue:value andPriority:priority withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ @try {
+ WAIT_FOR(done);
+ } @catch (NSException *exception) {
+ @throw [NSException exceptionWithName:@"DidNotSetValue" reason:@"Did not complete setting value"
+ userInfo:@{ @"ref": [ref description],
+ @"done": done ? @"true" : @"false",
+ @"value": (!value ? @"nil" : value),
+ @"priority": (!priority ? @"nil" : priority) }];
+ }
+}
+
+- (void)waitForCompletionOf:(FIRDatabaseReference *)ref updateChildValues:(NSDictionary *)values {
+ __block BOOL done = NO;
+ [ref updateChildValues:values withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ @try {
+ WAIT_FOR(done);
+ } @catch (NSException *exception) {
+ @throw [NSException exceptionWithName:@"DidNotUpdateChildValues" reason:@"Could not finish updating child values"
+ userInfo:@{ @"ref": [ref description],
+ @"values": (!values ? @"nil" : values)}];
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestCachePolicy.h b/Example/Database/Tests/Helpers/FTestCachePolicy.h
new file mode 100644
index 0000000..688c21d
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestCachePolicy.h
@@ -0,0 +1,27 @@
+/*
+ * 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 "FCachePolicy.h"
+
+@interface FTestCachePolicy : NSObject<FCachePolicy>
+
+- (id)initWithPercent:(float)percent maxQueries:(NSUInteger)maxQueries;
+
+- (void)pruneOnNextCheck;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestCachePolicy.m b/Example/Database/Tests/Helpers/FTestCachePolicy.m
new file mode 100644
index 0000000..aacd010
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestCachePolicy.m
@@ -0,0 +1,65 @@
+/*
+ * 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 "FTestCachePolicy.h"
+
+@interface FTestCachePolicy ()
+
+
+@property (nonatomic) float percentOfQueries;
+@property (nonatomic) NSUInteger maxTrackedQueries;
+@property (nonatomic) BOOL pruneNext;
+
+@end
+
+@implementation FTestCachePolicy
+
+- (id)initWithPercent:(float)percent maxQueries:(NSUInteger)maxQueries {
+ self = [super init];
+ if (self != nil) {
+ self->_maxTrackedQueries = maxQueries;
+ self->_percentOfQueries = percent;
+ self->_pruneNext = NO;
+ }
+ return self;
+}
+
+- (void)pruneOnNextCheck {
+ self.pruneNext = YES;
+}
+
+- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries {
+ if (self.pruneNext) {
+ self.pruneNext = NO;
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck {
+ return YES;
+}
+
+- (float)percentOfQueriesToPruneAtOnce {
+ return self.percentOfQueries;
+}
+
+- (NSUInteger)maxNumberOfQueriesToKeep {
+ return self.maxTrackedQueries;
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestClock.h b/Example/Database/Tests/Helpers/FTestClock.h
new file mode 100644
index 0000000..5520c6a
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestClock.h
@@ -0,0 +1,28 @@
+/*
+ * 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 "FClock.h"
+
+@interface FTestClock : NSObject<FClock>
+
+@property (nonatomic, readonly) NSTimeInterval currentTime;
+
+- (id)init;
+- (void)tick;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestClock.m b/Example/Database/Tests/Helpers/FTestClock.m
new file mode 100644
index 0000000..43599ac
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestClock.m
@@ -0,0 +1,33 @@
+/*
+ * 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 "FTestClock.h"
+
+@implementation FTestClock
+
+- (id)init {
+ self = [super init];
+ if (self != nil) {
+ self->_currentTime = 0.001;
+ }
+ return self;
+}
+
+- (void)tick {
+ self->_currentTime = self->_currentTime + 0.001;
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestContants.h b/Example/Database/Tests/Helpers/FTestContants.h
new file mode 100644
index 0000000..bc8dd8d
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestContants.h
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+#ifndef Firebase_FTestContants_h
+#define Firebase_FTestContants_h
+
+#define kFirebaseTestTimeout 7
+#define kFirebaseTestWaitUntilTimeout 5
+
+#endif
diff --git a/Example/Database/Tests/Helpers/FTestExpectations.h b/Example/Database/Tests/Helpers/FTestExpectations.h
new file mode 100644
index 0000000..8a797c8
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestExpectations.h
@@ -0,0 +1,32 @@
+/*
+ * 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 "FIRDatabaseQuery.h"
+
+@interface FTestExpectations : XCTestCase {
+ NSMutableArray* expectations;
+ XCTestCase* from;
+}
+
+- (id) initFrom:(XCTestCase *)other;
+- (void)addQuery:(FIRDatabaseQuery *)query withExpectation:(id)expectation;
+- (void) validate;
+
+@property (readonly) BOOL isReady;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestExpectations.m b/Example/Database/Tests/Helpers/FTestExpectations.m
new file mode 100644
index 0000000..d0f84d7
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestExpectations.m
@@ -0,0 +1,88 @@
+/*
+ * 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 "FTestExpectations.h"
+#import "FIRDataSnapshot.h"
+
+@interface FExpectation : NSObject
+
+@property (strong, nonatomic) FIRDatabaseQuery * query;
+@property (strong, nonatomic) id expectation;
+@property (strong, nonatomic) FIRDataSnapshot * snap;
+
+@end
+
+@implementation FExpectation
+
+@synthesize query;
+@synthesize expectation;
+@synthesize snap;
+
+@end
+
+@implementation FTestExpectations
+
+- (id) initFrom:(XCTestCase *)other {
+ self = [super init];
+ if (self) {
+ expectations = [[NSMutableArray alloc] init];
+ from = other;
+ }
+ return self;
+}
+
+- (void)addQuery:(FIRDatabaseQuery *)query withExpectation:(id)expectation {
+ FExpectation* exp = [[FExpectation alloc] init];
+ exp.query = query;
+ exp.expectation = expectation;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ exp.snap = snapshot;
+ }];
+ [expectations addObject:exp];
+}
+
+- (BOOL) isReady {
+ for (FExpectation* exp in expectations) {
+ if (!exp.snap) {
+ return NO;
+ }
+ // Note that a failure here will end up triggering the timeout
+ FIRDataSnapshot * snap = exp.snap;
+ NSDictionary* result = snap.value;
+ NSDictionary* expected = exp.expectation;
+ if ([result isEqual:[NSNull null]] || ![result isEqualToDictionary:expected]) {
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (void) validate {
+ for (FExpectation* exp in expectations) {
+ FIRDataSnapshot * snap = exp.snap;
+ NSDictionary* result = [snap value];
+ NSDictionary* expected = exp.expectation;
+ XCTAssertTrue([result isEqualToDictionary:expected], @"Expectation mismatch: %@ should be %@", result, expected);
+ }
+}
+
+- (void) failWithException:(NSException *) anException {
+ @throw anException;
+ // TODO: fix
+ //[from failWithException:anException];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestHelpers.h b/Example/Database/Tests/Helpers/FTestHelpers.h
new file mode 100644
index 0000000..679be7e
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestHelpers.h
@@ -0,0 +1,38 @@
+/*
+ * 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 "FTupleFirebase.h"
+#import "FRepoManager.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FTestContants.h"
+#import "FSnapshotUtilities.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
++ (FIRDatabaseReference *) getRandomNode;
++ (FIRDatabaseReference *) getRandomNodeWithoutPersistence;
++ (FTupleFirebase *) getRandomNodePair;
++ (FTupleFirebase *) getRandomNodePairWithoutPersistence;
++ (FTupleFirebase *) getRandomNodeTriple;
++ (id<FNode>)leafNodeOfSize:(NSUInteger)size;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTestHelpers.m b/Example/Database/Tests/Helpers/FTestHelpers.m
new file mode 100644
index 0000000..8ffdc7d
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTestHelpers.m
@@ -0,0 +1,132 @@
+/*
+ * 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 "FTestHelpers.h"
+#import "FConstants.h"
+#import "FIRApp.h"
+#import "FIROptions.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FTestAuthTokenGenerator.h"
+
+@implementation FTestHelpers
+
++ (NSTimeInterval) waitUntil:(BOOL (^)())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);
+}
+
++ (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;
+
+ id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]];
+
+ while (num > refs.count) {
+ NSString *sessionIdentifier = [NSString stringWithFormat:@"test-config-%@persistence-%lu", (persistence) ? @"" : @"no-", refs.count];
+ FIRDatabaseConfig *config = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:sessionIdentifier authTokenProvider:authTokenProvider];
+ config.persistenceEnabled = persistence;
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:config];
+ [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
diff --git a/Example/Database/Tests/Helpers/FTupleEventTypeString.h b/Example/Database/Tests/Helpers/FTupleEventTypeString.h
new file mode 100644
index 0000000..adcb4a0
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTupleEventTypeString.h
@@ -0,0 +1,33 @@
+/*
+ * 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 "FIRDataEventType.h"
+#import "FIRDatabaseReference.h"
+#import "FTypedefs.h"
+
+@interface FTupleEventTypeString : NSObject
+
+- (id)initWithFirebase:(FIRDatabaseReference *)f withEvent:(FIRDataEventType)evt withString:(NSString *)str;
+- (BOOL) isEqualTo:(FTupleEventTypeString *)other;
+
+@property (nonatomic, strong) FIRDatabaseReference * firebase;
+@property (readwrite) FIRDataEventType eventType;
+@property (nonatomic, strong) NSString* string;
+@property (nonatomic, copy) fbt_void_void vvcallback;
+@property (nonatomic) BOOL initialized;
+
+@end
diff --git a/Example/Database/Tests/Helpers/FTupleEventTypeString.m b/Example/Database/Tests/Helpers/FTupleEventTypeString.m
new file mode 100644
index 0000000..4cb3df2
--- /dev/null
+++ b/Example/Database/Tests/Helpers/FTupleEventTypeString.m
@@ -0,0 +1,53 @@
+/*
+ * 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 "FTupleEventTypeString.h"
+
+@implementation FTupleEventTypeString
+
+@synthesize firebase;
+@synthesize eventType;
+@synthesize string;
+@synthesize vvcallback;
+@synthesize initialized;
+
+- (id)initWithFirebase:(FIRDatabaseReference *)f withEvent:(FIRDataEventType)evt withString:(NSString *)str;
+{
+ self = [super init];
+ if (self) {
+ self.firebase = f;
+ self.eventType = evt;
+ self.string = str;
+ self.initialized = NO;
+ }
+ return self;
+}
+
+- (NSString *) description {
+ return [NSString stringWithFormat:@"%@ %@ (%zd)", self.firebase, self.string, self.eventType];
+}
+
+- (BOOL) isEqualTo:(FTupleEventTypeString *)other {
+ BOOL stringsEqual = NO;
+ if (self.string == nil && other.string == nil) {
+ stringsEqual = YES;
+ } else if (self.string != nil && other.string != nil) {
+ stringsEqual = [self.string isEqualToString:other.string];
+ }
+ return self.eventType == other.eventType && stringsEqual && [[self.firebase description] isEqualToString:[other.firebase description]];
+}
+
+@end
diff --git a/Example/Database/Tests/Helpers/SenTest+FWaiter.h b/Example/Database/Tests/Helpers/SenTest+FWaiter.h
new file mode 100644
index 0000000..81556df
--- /dev/null
+++ b/Example/Database/Tests/Helpers/SenTest+FWaiter.h
@@ -0,0 +1,26 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+@interface XCTest (FWaiter)
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate;
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate description:(NSString*)desc;
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds;
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds description:(NSString*)desc;
+
+@end
diff --git a/Example/Database/Tests/Helpers/SenTest+FWaiter.m b/Example/Database/Tests/Helpers/SenTest+FWaiter.m
new file mode 100644
index 0000000..4c5c854
--- /dev/null
+++ b/Example/Database/Tests/Helpers/SenTest+FWaiter.m
@@ -0,0 +1,57 @@
+/*
+ * 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 "SenTest+FWaiter.h"
+#import "FTestContants.h"
+
+@implementation XCTestCase (FWaiter)
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate {
+ return [self waitUntil:predicate timeout:kFirebaseTestWaitUntilTimeout description:nil];
+}
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate description:(NSString*)desc {
+ return [self waitUntil:predicate timeout:kFirebaseTestWaitUntilTimeout description:desc];
+}
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds {
+ return [self waitUntil:predicate timeout:seconds description:nil];
+}
+
+- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds description:(NSString*)desc {
+ 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];
+ if (currentTime > timeoutTime) {
+ if (desc != nil) {
+ XCTFail("Timed out on: %@", desc);
+ } else {
+ XCTFail("Timed out");
+ }
+ }
+ return (finish - start);
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FConnectionTest.m b/Example/Database/Tests/Integration/FConnectionTest.m
new file mode 100644
index 0000000..e72f6e4
--- /dev/null
+++ b/Example/Database/Tests/Integration/FConnectionTest.m
@@ -0,0 +1,77 @@
+/*
+ * 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 "FIRApp.h"
+#import "FIROptions.h"
+#import "FTestHelpers.h"
+#import "FConnection.h"
+#import "FTestBase.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@interface FConnectionTest : FTestBase
+
+@end
+
+@interface FTestConnectionDelegate : NSObject<FConnectionDelegate>
+
+@property (nonatomic, copy) void (^onReady)(NSString *);
+@property (nonatomic, copy) void (^onDisconnect)(FDisconnectReason);
+
+@end
+
+@implementation FTestConnectionDelegate
+
+- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID{
+ self.onReady(sessionID);
+}
+- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message {}
+- (void)onDisconnect:(FConnection *)fwebSocket withReason:(FDisconnectReason)reason {
+ self.onDisconnect(reason);
+}
+- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason {}
+
+@end
+@implementation FConnectionTest
+
+-(void) XXXtestObtainSessionId {
+ NSString* host = [NSString stringWithFormat:@"%@.firebaseio.com", [[FIRApp defaultApp] options].projectID];
+ FRepoInfo *info = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:@"default"];
+ FConnection *conn = [[FConnection alloc] initWith:info andDispatchQueue:[FIRDatabaseQuery sharedQueue] lastSessionID:nil];
+ FTestConnectionDelegate *delegate = [[FTestConnectionDelegate alloc] init];
+
+ __block BOOL done = NO;
+
+ delegate.onDisconnect = ^(FDisconnectReason reason) {
+ if (reason == DISCONNECT_REASON_SERVER_RESET) {
+ // It is very likely that the first connection attempt sends us a redirect to the project's designated server.
+ // We need follow that redirect before 'onReady' is invoked.
+ [conn open];
+ }
+ };
+ delegate.onReady = ^(NSString *sessionID) {
+ NSAssert(sessionID, @"sessionID cannot be null");
+ NSAssert([sessionID length] != 0, @"sessionID must have length > 0");
+ done = YES;
+ };
+
+ conn.delegate = delegate;
+ [conn open];
+
+ WAIT_FOR(done);
+}
+@end
diff --git a/Example/Database/Tests/Integration/FData.h b/Example/Database/Tests/Integration/FData.h
new file mode 100644
index 0000000..ebb502e
--- /dev/null
+++ b/Example/Database/Tests/Integration/FData.h
@@ -0,0 +1,22 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FData : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FData.m b/Example/Database/Tests/Integration/FData.m
new file mode 100644
index 0000000..390522c
--- /dev/null
+++ b/Example/Database/Tests/Integration/FData.m
@@ -0,0 +1,2687 @@
+/*
+ * 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 "FData.h"
+#import "FTestHelpers.h"
+#import "FEventTester.h"
+#import "FTupleEventTypeString.h"
+#import "FIRApp.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIROptions.h"
+#import "FRepo_Private.h"
+#import <limits.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 = [[FIRDatabase database] referenceFromURL:[self.databaseURL uppercaseString]];
+ FIRDatabaseReference * ref2 = [[FIRDatabase database] 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 = [[[FIRDatabase database] 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];
+}
+
+- (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;
+ }];
+}
+
+- (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 observeEventType: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;
+ }];
+}
+
+- (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;
+ }];
+}
+
+- (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");
+}
+
+- (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;
+ }];
+}
+
+- (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* expectedPriorites = @[[NSNull null], @10];
+ XCTAssertTrue([values isEqualToArray:expectedValues], @"Expected both listeners to get 5, got %@ instead", values);
+ XCTAssertTrue([priorities isEqualToArray:expectedPriorites], @"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];
+
+ XCTAssertTrue([[parent description] isEqualToString:self.databaseURL], @"Expect domain");
+ FIRDatabaseReference * child = [parent child:@"a/b/c"];
+ NSString* expected = [NSString stringWithFormat:@"%@/a/b/c", self.databaseURL];
+ XCTAssertTrue([[child description] isEqualToString:expected], @"Expected path");
+}
+
+- (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 = [[FIRDatabase database] 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 = [[FIRDatabase database] 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 = [[FIRDatabase database] 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 = [[FIRDatabase database] 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;
+ }];
+}
+
+- (void) testUpdateReplacesChildrenAndIsNotRecursive {
+ 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;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+ ready = NO;
+
+ [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *result = [snapshot value];
+ NSDictionary *expected = @{@"a" : @{@"aa" : @10, @"ab" : @20}};
+ XCTAssertTrue([result isEqualToDictionary:expected], @"Should get new value");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ NSDictionary* result = [localSnap value];
+ NSDictionary* expected = @{@"a": @{@"aa": @10, @"ab": @20}};
+ return ready && [result isEqualToDictionary:expected];
+ }];
+}
+
+// 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 = 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");
+}
+
+- (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);
+}
+
+- (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) testLocalServerValuesEventuallyButNotImmediatelyMatchServer {
+ 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) testServerValuesSetWithPriorityRemoteEvents {
+ 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) testServerValuesSetPriorityRemoteEvents {
+ 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) testServerValuesUpdateRemoteEvents {
+ 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) testServerValuesSetWithPriorityLocalEvents {
+ 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) testServerValuesSetPriorityLocalEvents {
+ 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) testServerValuesUpdateLocalEvents {
+ 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) testServerValuesTransactionLocalEvents {
+ 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) 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 = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseReference * ref2 = [[[FIRDatabaseReference alloc] initWithConfig:cfg] child: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) testServerValuesEventualConsistencyBetweenLocalAndRemote {
+ 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
diff --git a/Example/Database/Tests/Integration/FDotInfo.h b/Example/Database/Tests/Integration/FDotInfo.h
new file mode 100644
index 0000000..73bd4c7
--- /dev/null
+++ b/Example/Database/Tests/Integration/FDotInfo.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FDotInfo : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FDotInfo.m b/Example/Database/Tests/Integration/FDotInfo.m
new file mode 100644
index 0000000..0245dc5
--- /dev/null
+++ b/Example/Database/Tests/Integration/FDotInfo.m
@@ -0,0 +1,173 @@
+/*
+ * 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 "FDotInfo.h"
+#import "FTestHelpers.h"
+#import "FIRDatabaseConfig_Private.h"
+
+@implementation FDotInfo
+
+- (void) testCanGetReferenceToInfoNodes {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref.root child:@".info"];
+ [ref.root child:@".info/foo"];
+}
+
+- (void) testCantWriteToInfo {
+ FIRDatabaseReference * ref = [[FTestHelpers getRandomNode].root child:@".info"];
+ XCTAssertThrows([ref setValue:@"hi"], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref setValue:@"hi" andPriority:@5], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref setPriority:@"hi"], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }], @"Cannot write to path at /.info");
+ XCTAssertThrows([ref removeValue], @"Cannot write to path at /.info");
+ XCTAssertThrows([[ref child:@"test"] setValue:@"hi"], @"Cannot write to path at /.info");
+}
+
+- (void) testCanWatchInfoConnected {
+ FIRDatabaseReference * rootRef = [FTestHelpers getRandomNode].root;
+ __block BOOL done = NO;
+ [[rootRef child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([[snapshot value] boolValue]) {
+ done = YES;
+ }
+ }];
+ [self waitUntil:^{ return done; }];
+}
+
+- (void) testInfoConnectedGoesToFalseOnDisconnect {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseReference * rootRef = [[FIRDatabaseReference alloc] initWithConfig:cfg];
+ __block BOOL everConnected = NO;
+ __block NSMutableString *connectedHistory = [[NSMutableString alloc] init];
+ [[rootRef child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([[snapshot value] boolValue]) {
+ everConnected = YES;
+ }
+
+ if (everConnected) {
+ [connectedHistory appendString:([[snapshot value] boolValue] ? @"YES," : @"NO,")];
+ }
+ }];
+ [self waitUntil:^{ return everConnected; }];
+
+ [FRepoManager interrupt:cfg];
+ [FRepoManager resume:cfg];
+
+ [self waitUntil:^BOOL{
+ return [connectedHistory isEqualToString:@"YES,NO,YES,"];
+ }];
+
+ [FRepoManager interrupt:cfg];
+ [FRepoManager disposeRepos:cfg];
+}
+
+- (void) testInfoServerTimeOffset {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:cfg];
+
+ // make sure childByAutoId works
+ [ref childByAutoId];
+
+ NSMutableArray* offsets = [[NSMutableArray alloc] init];
+
+ [[ref child:@".info/serverTimeOffset"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSLog(@"got value: %@", snapshot.value);
+ [offsets addObject:snapshot.value];
+ }];
+
+ WAIT_FOR(offsets.count == 1);
+
+ XCTAssertTrue([[offsets objectAtIndex:0] isKindOfClass:[NSNumber class]], @"Second element should be a number, in milliseconds");
+
+ // make sure childByAutoId still works
+ [ref childByAutoId];
+
+ [FRepoManager interrupt:cfg];
+ [FRepoManager disposeRepos:cfg];
+}
+
+- (void) testManualConnectionManagement {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"];
+ FIRDatabaseConfig *altCfg = [FIRDatabaseConfig configForName:@"alt-config"];
+
+ FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:cfg];
+ FIRDatabaseReference * refAlt = [[FIRDatabaseReference alloc] initWithConfig:altCfg];
+
+ // Wait until we're connected to both Firebases
+ __block BOOL ready = NO;
+ [[ref child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[ref child:@".info/connected"] removeAllObservers];
+
+ ready = NO;
+ [[refAlt child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[refAlt child:@".info/connected"] removeAllObservers];
+
+ [FIRDatabaseReference goOffline];
+
+ // Ensure we're disconnected from both Firebases
+ ready = NO;
+
+ [[ref child:@".info/connected"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse([[snapshot value] boolValue], @".info/connected should be false");
+ ready = YES;
+ }];
+ [self waitUntil:^{ return ready; }];
+ ready = NO;
+ [[refAlt child:@".info/connected"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertFalse([[snapshot value] boolValue], @".info/connected should be false");
+ ready = YES;
+ }];
+ [self waitUntil:^{ return ready; }];
+
+ // Ensure that we don't automatically reconnect upon new Firebase creation
+ FIRDatabaseReference * refDup = [[FIRDatabaseReference alloc] initWithConfig:altCfg];
+ [[refDup child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([[snapshot value] boolValue]) {
+ XCTFail(@".info/connected should remain false");
+ }
+ }];
+
+ // Wait for 1.5 seconds to make sure connected remains false
+ [NSThread sleepForTimeInterval:1.5];
+ [[refDup child:@".info/connected"] removeAllObservers];
+
+ [FIRDatabaseReference goOnline];
+
+ // Ensure we're reconnected to both Firebases
+ ready = NO;
+ [[ref child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[ref child:@".info/connected"] removeAllObservers];
+
+ ready = NO;
+ [[refAlt child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^{ return ready; }];
+ [[refAlt child:@".info/connected"] removeAllObservers];
+}
+@end
diff --git a/Example/Database/Tests/Integration/FEventTests.h b/Example/Database/Tests/Integration/FEventTests.h
new file mode 100644
index 0000000..8ea5eef
--- /dev/null
+++ b/Example/Database/Tests/Integration/FEventTests.h
@@ -0,0 +1,24 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FEventTests : FTestBase {
+ BOOL rl;
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FEventTests.m b/Example/Database/Tests/Integration/FEventTests.m
new file mode 100644
index 0000000..8b11e9d
--- /dev/null
+++ b/Example/Database/Tests/Integration/FEventTests.m
@@ -0,0 +1,506 @@
+/*
+ * 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 "FEventTests.h"
+#import "FTestHelpers.h"
+#import "FTupleEventTypeString.h"
+#import "FEventTester.h"
+
+@implementation FEventTests
+
+
+
+- (void) testInvalidEventType {
+ FIRDatabaseReference * f = [FTestHelpers getRandomNode];
+ XCTAssertThrows([f observeEventType:-4 withBlock:^(FIRDataSnapshot *s) {}], @"Invalid event type properly throws an error");
+}
+
+- (void) testWriteLeafExpectValueChanged {
+
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ __block BOOL done = NO;
+ [writeNode setValue:@1234 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+
+ [super snapWaiter:readNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([s value], @1234, @"Proper value in snapshot");
+ }];
+}
+
+- (void) testWRiteLeafNodeThenExpectValueEvent {
+ FIRDatabaseReference * writeNode = [FTestHelpers getRandomNode];
+ [writeNode setValue:@42];
+
+ [super snapWaiter:writeNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([s value], @42, @"Proper value in snapshot");
+ }];
+
+}
+
+- (void) testWriteLeafNodeThenExpectChildAddedEventThenValueEvent {
+
+ FIRDatabaseReference * writeNode = [FTestHelpers getRandomNode];
+
+ [[writeNode child:@"foo"] setValue:@878787];
+
+ NSArray* lookingFor = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeChildAdded withString:@"foo"],
+ [[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:lookingFor];
+ [et wait];
+
+ [super snapWaiter:writeNode withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertEqualObjects([[s childSnapshotForPath:@"foo"] value], @878787, @"Got proper value");
+ }];
+
+}
+
+- (void) testWriteTwoNestedLeafNodesChange {
+
+}
+
+- (void) testSetMultipleEventListenersOnSameNode {
+
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ [writeNode setValue:@42];
+
+ // two write nodes
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+ et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+ // two read nodes
+ et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:readNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+ et = [[FEventTester alloc] initFrom:self];
+ [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:readNode withEvent:FIRDataEventTypeValue withString:nil] ]];
+ [et wait];
+
+}
+
+- (void) testUnsubscribeEventsAndConfirmThatEventsNoLongerFire {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block int numValueCB = 0;
+
+ FIRDatabaseHandle handle = [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ numValueCB = numValueCB + 1;
+ }];
+
+ // Set
+ for(int i = 0; i < 3; i++) {
+ [node setValue:[NSNumber numberWithInt:i]];
+ }
+
+ // bye
+ [node removeObserverWithHandle:handle];
+
+ // set again
+ for(int i = 10; i < 15; i++) {
+ [node setValue:[NSNumber numberWithInt:i]];
+ }
+
+ for(int i = 20; i < 25; i++) {
+ [node setValue:[NSNumber numberWithInt:i]];
+ }
+
+ // Should just be 3
+ [self waitUntil:^BOOL{
+ return numValueCB == 3;
+ }];
+}
+
+- (void) testCanWriteACompoundObjectAndGetMoreGranularEventsForIndividualChanges {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writeNode = tuple.one;
+ FIRDatabaseReference * readNode = tuple.two;
+
+ __block BOOL done = NO;
+ [writeNode setValue:@{@"a": @10, @"b": @20} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{ return done; }];
+
+ NSArray* lookingForW = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ NSArray* lookingForR = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil],
+ ];
+
+ FEventTester* etW = [[FEventTester alloc] initFrom:self];
+ [etW addLookingFor:lookingForW];
+ [etW wait];
+
+ FEventTester* etR = [[FEventTester alloc] initFrom:self];
+ [etR addLookingFor:lookingForR];
+ [etR wait];
+
+ // Modify compound but just change one of them
+
+ lookingForW = @[[[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] ];
+ lookingForR = @[[[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] ];
+
+ [etW addLookingFor:lookingForW];
+ [etR addLookingFor:lookingForR];
+
+ [writeNode setValue:@{@"a": @10, @"b": @30}];
+
+ [etW wait];
+ [etR wait];
+}
+
+
+- (void) testValueEventIsFiredForEmptyNode {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL valueFired = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ XCTAssertTrue([[s value] isEqual:[NSNull null]], @"Value is properly nil");
+ valueFired = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return valueFired;
+ }];
+}
+
+- (void) testCorrectEventsRaisedWhenLeafTurnsIntoInternalNode {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ NSMutableString* eventString = [[NSMutableString alloc] init];
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ if ([s hasChildren]) {
+ [eventString appendString:@", got children"];
+ }
+ else {
+ [eventString appendFormat:@", value %@", [s value]];
+ }
+ }];
+
+ [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *s) {
+ [eventString appendFormat:@", child_added %@", [s key]];
+ }];
+
+ [node setValue:@42];
+ [node setValue:@{@"a": @2}];
+ [node setValue:@84];
+ __block BOOL done = NO;
+ [node setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+
+ XCTAssertEqualObjects(@", value 42, child_added a, got children, value 84, value <null>", eventString, @"Proper order seen");
+}
+
+- (void) testRegisteringCallbackMultipleTimesAndUnregistering {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block int changes = 0;
+
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) { changes = changes + 1; };
+
+ FIRDatabaseHandle handle1 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+ FIRDatabaseHandle handle2 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+ FIRDatabaseHandle handle3 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+
+ __block BOOL done = NO;
+
+ [node setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 3, @"Saw 3 callback events %d", changes);
+
+ [node removeObserverWithHandle:handle1];
+ [node setValue:@84 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 5, @"Saw 5 callback events %d", changes);
+
+ [node removeObserverWithHandle:handle2];
+ [node setValue:@168 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 6, @"Saw 6 callback events %d", changes);
+
+ [node removeObserverWithHandle:handle3];
+ [node setValue:@376 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+ done = NO;
+
+ XCTAssertTrue(changes == 6, @"Saw 6 callback events %d", changes);
+
+ NSLog(@"callbacks: %d", changes);
+
+}
+
+- (void) testUnregisteringTheSameCallbackTooManyTimesDoesNothing {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) { };
+
+ FIRDatabaseHandle handle1 = [node observeEventType:FIRDataEventTypeValue withBlock:cb];
+ [node removeObserverWithHandle:handle1];
+ [node removeObserverWithHandle:handle1];
+
+ XCTAssertTrue(YES, @"Properly reached end of test without throwing errors.");
+}
+
+- (void) testOnceValueFiresExactlyOnce {
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+
+ [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ firstCall = NO;
+ XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value");
+ }];
+
+ [path setValue:@42];
+ [path setValue:@84];
+
+ __block BOOL done = NO;
+
+ [path setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+- (void) testOnceChildAddedFiresExaclyOnce {
+ __block int badCount = 0;
+
+ // for(int i = 0; i < 100; i++) {
+
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+
+ __block BOOL done = NO;
+
+
+ [path observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ firstCall = NO;
+ XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value");
+ XCTAssertEqualObjects(@"foo", [snapshot key], @"Properly saw the first node");
+ if (![[snapshot value] isEqual:@42]) {
+ exit(-1);
+ badCount = badCount + 1;
+ }
+
+ done = YES;
+
+
+ }];
+
+ [[path child:@"foo"] setValue:@42];
+ [[path child:@"bar"] setValue:@84]; // XXX FIXME sometimes this event fires first
+ [[path child:@"foo"] setValue:@168];
+
+
+// [path setValue:nil withCompletionBlock:^(BOOL status) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+
+
+ // }
+
+ NSLog(@"BADCOUNT: %d", badCount);
+}
+
+- (void) testOnceValueFiresExacltyOnceEvenIfThereIsASetInsideCallback {
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+ __block BOOL done = NO;
+
+ [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ if (firstCall) {
+ firstCall = NO;
+ XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value");
+ [path setValue:@43];
+ done = YES;
+ }
+ else {
+ XCTFail(@"Callback got called more than once.");
+ }
+ }];
+
+ [path setValue:@42];
+ [path setValue:@84];
+
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+- (void) testOnceChildAddedFiresOnceEvenWithCompoundObject {
+ FIRDatabaseReference * path = [FTestHelpers getRandomNode];
+ __block BOOL firstCall = YES;
+
+ [path observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(firstCall, @"Properly saw first call");
+ firstCall = NO;
+ XCTAssertEqualObjects(@84, [snapshot value], @"Properly saw node value");
+ XCTAssertEqualObjects(@"bar", [snapshot key], @"Properly saw the first node");
+ }];
+
+ [path setValue:@{@"foo": @42, @"bar": @84}];
+
+ __block BOOL done = NO;
+
+ [path setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }];
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+- (void) testOnEmptyChildFires {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ [[node child:@"test"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"Properly saw nil child node");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{ return done; }];
+}
+
+
+- (void) testOnEmptyChildEvenAfterParentIsSynched {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ __block BOOL parentDone = NO;
+ __block BOOL done = NO;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ parentDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return parentDone;
+ }];
+
+ [[node child:@"test"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"Child is properly nil");
+ done = YES;
+ }];
+
+ // This test really isn't in the same spirit as the JS test; we can't currently make sure that the test fires right away since the ON and callback are async
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(done, @"Done fired.");
+}
+
+- (void) testEventsAreRaisedChildRemovedChildAddedChildMoved {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+
+ [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"added %@", [snap key]]];
+ }];
+
+ [node observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"removed %@", [snap key]]];
+ }];
+
+ [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"moved %@", [snap key]]];
+ }];
+
+ __block BOOL done = NO;
+
+ [node setValue:@{
+ @"a": @{@".value": @1, @".priority": @0 },
+ @"b": @{@".value": @1, @".priority": @1 },
+ @"c": @{@".value": @1, @".priority": @2 },
+ @"d": @{@".value": @1, @".priority": @3 },
+ @"e": @{@".value": @1, @".priority": @4 },
+ @"f": @{@".value": @1, @".priority": @5 },
+ } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [events removeAllObjects];
+
+ done = NO;
+
+ [node setValue:@{
+ @"a": @{@".value": @1, @".priority": @5 },
+ @"aa": @{@".value": @1, @".priority": @0 },
+ @"b": @{@".value": @1, @".priority": @1 },
+ @"bb": @{@".value": @1, @".priority": @2 },
+ @"d": @{@".value": @1, @".priority": @3 },
+ @"e": @{@".value": @1, @".priority": @6 },
+ }
+ withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }
+ ];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertEqualObjects(@"removed c, removed f, added aa, added bb, moved a, moved e", [events componentsJoinedByString:@", "], @"Got expected results");
+}
+
+- (void) testIntegerToDoubleConversions {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableArray<NSString *>* events = [[NSMutableArray alloc] init];
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snap) {
+ [events addObject:[NSString stringWithFormat:@"value %@", [snap value]]];
+ }];
+
+ for(NSNumber *number in @[@1, @1.0, @1, @1.1]) {
+ [self waitForCompletionOf:node setValue:number];
+ }
+
+ XCTAssertEqualObjects(@"value 1, value 1.1", [events componentsJoinedByString:@", "],
+ @"Got expected results");
+
+}
+
+- (void) testEventsAreRaisedProperlyWithOnQueryLimits {
+ // xxx impl query
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FIRAuthTests.m b/Example/Database/Tests/Integration/FIRAuthTests.m
new file mode 100644
index 0000000..2c44580
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRAuthTests.m
@@ -0,0 +1,67 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FIRApp.h"
+#import "FTestHelpers.h"
+#import "FTestAuthTokenGenerator.h"
+#import "FIRTestAuthTokenProvider.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FTestBase.h"
+
+@interface FIRAuthTests : FTestBase
+
+@end
+
+@implementation FIRAuthTests
+
+- (void)setUp {
+ [super setUp];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+- (void)testListensAndAuthRaceCondition {
+ [FIRDatabase setLoggingEnabled:YES];
+ id<FAuthTokenProvider> tokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]];
+
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:@"testWritesRestoredAfterAuth"];
+ config.authTokenProvider = tokenProvider;
+
+ FIRDatabaseReference *ref = [[[FIRDatabaseReference alloc] initWithConfig:config] childByAutoId];
+
+ __block BOOL done = NO;
+
+ [[[ref root] child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^void(
+ FIRDataSnapshot *snapshot) {
+ if ([snapshot.value boolValue]) {
+ // Start a listen before auth credentials are restored.
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ }];
+
+ // subsequent writes should complete successfully.
+ [ref setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ }
+ }];
+
+ WAIT_FOR(done);
+}
+@end
diff --git a/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h
new file mode 100644
index 0000000..d6074ac
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FIRDatabaseQueryTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m
new file mode 100644
index 0000000..a5bff5a
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m
@@ -0,0 +1,2780 @@
+/*
+ * 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 "FIRDatabaseQueryTests.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FQuerySpec.h"
+#import "FTestExpectations.h"
+
+@implementation FIRDatabaseQueryTests
+
+- (void) testCanCreateBasicQueries {
+ // Just make sure none of these throw anything
+
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref queryLimitedToFirst:10];
+ [ref queryLimitedToLast:10];
+
+ [[ref queryOrderedByKey] queryStartingAtValue:@"foo"];
+ [[ref queryOrderedByKey] queryEndingAtValue:@"foo"];
+ [[ref queryOrderedByKey] queryEqualToValue:@"foo"];
+
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@YES];
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@1];
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@"foo"];
+ [[ref queryOrderedByChild:@"index"] queryStartingAtValue:nil];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@YES];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@1];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@"foo"];
+ [[ref queryOrderedByChild:@"index"] queryEndingAtValue:nil];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:@YES];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:@1];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:@"foo"];
+ [[ref queryOrderedByChild:@"index"] queryEqualToValue:nil];
+
+ [[ref queryOrderedByPriority] queryStartingAtValue:@1];
+ [[ref queryOrderedByPriority] queryStartingAtValue:@"foo"];
+ [[ref queryOrderedByPriority] queryStartingAtValue:nil];
+ [[ref queryOrderedByPriority] queryEndingAtValue:@1];
+ [[ref queryOrderedByPriority] queryEndingAtValue:@"foo"];
+ [[ref queryOrderedByPriority] queryEndingAtValue:nil];
+ [[ref queryOrderedByPriority] queryEqualToValue:@1];
+ [[ref queryOrderedByPriority] queryEqualToValue:@"foo"];
+ [[ref queryOrderedByPriority] queryEqualToValue:nil];
+}
+
+- (void) testInvalidQueryParams {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ XCTAssertThrows([[ref queryLimitedToFirst:100] queryLimitedToFirst:100]);
+ XCTAssertThrows([[ref queryLimitedToFirst:100] queryLimitedToLast:100]);
+ XCTAssertThrows([[ref queryLimitedToLast:100] queryLimitedToFirst:100]);
+ XCTAssertThrows([[ref queryLimitedToLast:100] queryLimitedToLast:100]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByPriority]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByChild:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByValue]);
+ XCTAssertThrows([[ref queryStartingAtValue:@"foo"] queryStartingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryStartingAtValue:@"foo"] queryEqualToValue:@"foo"]);
+ XCTAssertThrows([[ref queryEndingAtValue:@"foo"] queryEndingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryEndingAtValue:@"foo"] queryEqualToValue:@"foo"]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryStartingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryEndingAtValue:@"foo"]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryEqualToValue:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@"foo" childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@"foo" childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:@"foo" childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@1 childKey:@"foo"]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@YES]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@1]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@YES]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:nil]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:nil]);
+ XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:nil]);
+ XCTAssertThrows([[ref queryStartingAtValue:@"foo" childKey:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEndingAtValue:@"foo" childKey:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEqualToValue:@"foo" childKey:@"foo"] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryStartingAtValue:@1] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryStartingAtValue:@YES] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEndingAtValue:@1] queryOrderedByKey]);
+ XCTAssertThrows([[ref queryEndingAtValue:@YES] queryOrderedByKey]);
+ XCTAssertThrows([ref queryStartingAtValue:@[]]);
+ XCTAssertThrows([ref queryStartingAtValue:@{}]);
+ XCTAssertThrows([ref queryEndingAtValue:@[]]);
+ XCTAssertThrows([ref queryEndingAtValue:@{}]);
+ XCTAssertThrows([ref queryEqualToValue:@[]]);
+ XCTAssertThrows([ref queryEqualToValue:@{}]);
+
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByPriority], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByPriority], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByKey], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByKey], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByChild:@"foo"], @"Cannot call orderBy multiple times");
+ XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByChild:@"foo"], @"Cannot call orderBy multiple times");
+
+ XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@"a" childKey:@"b"], @"Cannot specify starting child name when ordering by key.");
+ XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@"a" childKey:@"b"], @"Cannot specify ending child name when ordering by key.");
+ XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:@"a" childKey:@"b"], @"Cannot specify equalTo child name when ordering by key.");
+
+ XCTAssertThrows([[ref queryOrderedByPriority] queryStartingAtValue:@YES], @"Can't pass booleans as start/end when using priority index.");
+ XCTAssertThrows([[ref queryOrderedByPriority] queryEndingAtValue:@NO], @"Can't pass booleans as start/end when using priority index.");
+ XCTAssertThrows([[ref queryOrderedByPriority] queryEqualToValue:@YES], @"Can't pass booleans as start/end when using priority index.");
+}
+
+- (void) testLimitRanges
+{
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ XCTAssertThrows([ref queryLimitedToLast:0], @"Can't pass zero as limit");
+ XCTAssertThrows([ref queryLimitedToFirst:0], @"Can't pass zero as limit");
+ XCTAssertThrows([ref queryLimitedToLast:0], @"Can't pass zero as limit");
+ uint64_t MAX_ALLOWED_VALUE = (1l << 31) - 1;
+ [ref queryLimitedToFirst:MAX_ALLOWED_VALUE];
+ [ref queryLimitedToLast:MAX_ALLOWED_VALUE];
+ XCTAssertThrows([ref queryLimitedToFirst:(MAX_ALLOWED_VALUE+1)], @"Can't pass limits that don't fit into 32 bit signed integer range");
+ XCTAssertThrows([ref queryLimitedToLast:(MAX_ALLOWED_VALUE+1)], @"Can't pass limits that don't fit into 32 bit signed integer range");
+}
+
+- (void) testInvalidKeys {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSArray* badKeys = @[ @".test", @"test.", @"fo$o", @"[what", @"ever]", @"ha#sh", @"/thing", @"th/ing", @"thing/"];
+
+ for (NSString* badKey in badKeys) {
+ XCTAssertThrows([[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:badKey], @"Setting bad key");
+ XCTAssertThrows([[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:badKey], @"Setting bad key");
+ }
+}
+
+- (void) testOffCanBeCalledOnDefault {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (called) {
+ XCTFail(@"Should not be called twice");
+ } else {
+ called = YES;
+ }
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called;
+ }];
+
+ called = NO;
+
+ [ref removeAllObservers];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called, @"Should not have been called again");
+}
+
+- (void) testOffCanBeCalledOnHandle {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ FIRDatabaseHandle handle = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (called) {
+ XCTFail(@"Should not be called twice");
+ } else {
+ called = YES;
+ }
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called;
+ }];
+
+ called = NO;
+
+ [ref removeObserverWithHandle:handle];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called, @"Should not have been called again");
+}
+
+- (void) testOffCanBeCalledOnSpecificQuery {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ FIRDatabaseHandle handle = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (called) {
+ XCTFail(@"Should not be called twice");
+ } else {
+ called = YES;
+ }
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called;
+ }];
+
+ called = NO;
+
+ [query removeObserverWithHandle:handle];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called, @"Should not have been called again");
+}
+
+- (void) testOffCanBeCalledOnMultipleQueries {
+ FIRDatabaseQuery *query = [[FTestHelpers getRandomNode] queryLimitedToFirst:10];
+ FIRDatabaseHandle handle1 = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ FIRDatabaseHandle handle2 = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ [query removeObserverWithHandle:handle1];
+ [query removeObserverWithHandle:handle2];
+}
+
+- (void) testOffCanBeCalledWithoutHandle {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL called1 = NO;
+ __block BOOL called2 = NO;
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ called1 = YES;
+ }];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ called2 = YES;
+ }];
+
+ [ref setValue:@{@"a": @5, @"b": @6}];
+
+ [self waitUntil:^BOOL{
+ return called1 && called2;
+ }];
+
+ called1 = NO;
+ called2 = NO;
+
+ [ref removeAllObservers];
+
+ __block BOOL complete = NO;
+ [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ complete = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return complete;
+ }];
+
+ XCTAssertFalse(called1 || called2, @"Should not have called either callback");
+}
+
+- (void) testEnsureOnly5ItemsAreKept {
+ __block FIRDataSnapshot * snap = nil;
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ __block int count = 0;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ count++;
+ }];
+
+ [ref setValue:nil];
+ for (int i = 0; i < 10; ++i) {
+ [[ref childByAutoId] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ [self waitUntil:^BOOL{
+ // The initial set triggers the callback, so we need to wait for 11 events
+ return count == 11;
+ }];
+
+ count = 5;
+ for (FIRDataSnapshot * snapshot in snap.children) {
+ NSNumber* num = [snapshot value];
+ NSNumber* current = [NSNumber numberWithInt:count];
+ XCTAssertTrue([num isEqualToNumber:current], @"Expect children in order");
+ count++;
+ }
+
+ XCTAssertTrue(count == 10, @"Expected 5 children");
+}
+
+- (void) testOnlyLast5SentFromServer {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block int count = 0;
+
+ [ref setValue:nil];
+
+ for (int i = 0; i < 10; ++i) {
+ [[ref childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ count++;
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return count == 10;
+ }];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:5];
+ count = 5;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ NSNumber *num = [child value];
+ NSNumber *current = [NSNumber numberWithInt:count];
+ XCTAssertTrue([num isEqualToNumber:current], @"Expect children to be in order");
+ count++;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 10;
+ }];
+}
+
+- (void) testVariousLimits {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[ref queryLimitedToLast:1] withExpectation:@{@"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:1] withExpectation:@{@"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:2] withExpectation:@{@"b": @2, @"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:3] withExpectation:@{@"a": @1, @"b": @2, @"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:4] withExpectation:@{@"a": @1, @"b": @2, @"c": @3}];
+
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [expectations validate];
+}
+
+- (void) testSetLimitsWithStartAt {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1] withExpectation:@{@"a": @1}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"c"] queryLimitedToFirst:1] withExpectation:@{@"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:1] withExpectation:@{@"b": @2}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:2] withExpectation:@{@"b": @2, @"c": @3}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:3] withExpectation:@{@"b": @2, @"c": @3}];
+
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [expectations validate];
+}
+
+- (void) testLimitsAndStartAtWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1] withExpectation:@{@"a": @1}];
+
+ /*params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"c"];
+ params = [params limitTo:1];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"c": @3}];
+
+ params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"b"];
+ params = [params limitTo:1];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2}];
+
+ params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"b"];
+ params = [params limitTo:2];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2, @"c": @3}];
+
+ params = [[FQueryParams alloc] init];
+ params = [params setStartPriority:nil andName:@"b"];
+ params = [params limitTo:3];
+ [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2, @"c": @3}];*/
+
+ [self waitUntil:^BOOL{
+ return expectations.isReady;
+ }];
+ [expectations validate];
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHit {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"d"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add d");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHitWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"d"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add d");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHitWithStart {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"a", @"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"aa"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"aa"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add aa");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsAreFiredWhenLimitIsHitWithStartAndServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"a", @"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"aa"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"aa"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add aa");
+ [ref removeAllObservers];
+}
+
+- (void) testStartAndLimitWithIncompleteWindow {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready && [added count] >= 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Expected to remove nothing");
+ expected = @[@"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add b");
+ [ref removeAllObservers];
+}
+
+- (void) testStartAndLimitWithIncompleteWindowAndServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Expected to remove nothing");
+ expected = @[@"b"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add b");
+ [ref removeAllObservers];
+}
+
+- (void) testChildEventsFiredWhenItemDeleted {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready && [added count] >= 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ expected = @[@"a"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Expected to add a");
+ [ref removeAllObservers];
+}
+
+-(void) testChildEventsAreFiredWhenItemDeletedAtServer {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertEqualObjects(removed, (@[@"b"]), @"Expected to remove b");
+ XCTAssertEqualObjects(added, (@[@"a"]), @"Expected to add a");
+ [ref removeAllObservers];
+}
+
+- (void) testRemoveFiredWhenItemDeleted {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready && [added count] >= 1;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have one item");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ XCTAssertTrue([added count] == 0, @"Expected to add nothing");
+ [ref removeAllObservers];
+}
+
+-(void) testRemoveFiredWhenItemDeletedAtServer {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FIRDatabaseQuery * query = [ref queryLimitedToLast:2];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+ NSMutableArray* removed = [[NSMutableArray alloc] init];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [added addObject:[snapshot key]];
+ }];
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+
+ [removed addObject:[snapshot key]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return [added count] == 2;
+ }];
+
+ XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window");
+ NSArray* expected = @[@"b", @"c"];
+ XCTAssertTrue([added isEqualToArray:expected], @"Should have two items");
+
+ [added removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"b"];
+ XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b");
+ XCTAssertTrue([added count] == 0, @"Expected to add nothing");
+ [ref removeAllObservers];
+}
+
+- (void) testStartAtPriorityAndEndAtPriorityWork {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"y"] withExpectation:@{@"b": @2, @"c": @3, @"d": @4}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:@"a"] queryEndingAtValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testStartAtPriorityAndEndAtPriorityWorkWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"y"] withExpectation:@{@"b": @2, @"c": @3, @"d": @4}];
+ [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:@"a"] queryEndingAtValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWork {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"a"] queryEndingAtValue:@2 childKey:@"d"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"b"] queryEndingAtValue:@2 childKey:@"c"];
+ [expectations addQuery:query withExpectation:@{@"b": @2, @"c": @3}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"c": @3, @"d": @4}];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ }];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWorkWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"a"] queryEndingAtValue:@2 childKey:@"d"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"b"] queryEndingAtValue:@2 childKey:@"c"];
+ [expectations addQuery:query withExpectation:@{@"b": @2, @"c": @3}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"c": @3, @"d": @4}];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWork2 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2 childKey:@"b"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"d"] queryEndingAtValue:@2 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"d": @4, @"a": @1}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"e"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2}];
+
+ [ref setValue:@{
+ @"c": @{@".value": @3, @".priority": @1},
+ @"d": @{@".value": @4, @".priority": @1},
+ @"a": @{@".value": @1, @".priority": @2},
+ @"b": @{@".value": @2, @".priority": @2}
+ }];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testStartAtAndEndAtPriorityAndNameWorkWithServerData2 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"c": @{@".value": @3, @".priority": @1},
+ @"d": @{@".value": @4, @".priority": @1},
+ @"a": @{@".value": @1, @".priority": @2},
+ @"b": @{@".value": @2, @".priority": @2}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2 childKey:@"b"];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"d"] queryEndingAtValue:@2 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"d": @4, @"a": @1}];
+
+ query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"e"] queryEndingAtValue:@2];
+ [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2}];
+
+ WAIT_FOR(expectations.isReady);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[ref queryOrderedByPriority] queryEqualToValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityWorksWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @"z"},
+ @"b": @{@".value": @2, @".priority": @"y"},
+ @"c": @{@".value": @3, @".priority": @"x"},
+ @"d": @{@".value": @4, @".priority": @"w"}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ [expectations addQuery:[[ref queryOrderedByPriority] queryEqualToValue:@"w"] withExpectation:@{@"d": @4}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityAndNameWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[ref queryOrderedByPriority] queryEqualToValue:@1 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"a": @1}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"1" childKey:@"z"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testEqualToPriorityAndNameWorksWithServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL ready = NO;
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @1},
+ @"c": @{@".value": @3, @".priority": @2},
+ @"d": @{@".value": @4, @".priority": @2}
+ } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self];
+
+ FIRDatabaseQuery * query = [[ref queryOrderedByPriority] queryEqualToValue:@1 childKey:@"a"];
+ [expectations addQuery:query withExpectation:@{@"a": @1}];
+
+ __block id nullSnap = @"dummy";
+ [[[ref queryOrderedByPriority] queryEqualToValue:@"1" childKey:@"z"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nullSnap = [snapshot value];
+ }];
+
+ WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]);
+
+ [expectations validate];
+}
+
+- (void) testPrevNameWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* added = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [added addObject:snapshot.key];
+ if (prevName) {
+ [added addObject:prevName];
+ } else {
+ [added addObject:@"null"];
+ }
+
+ }];
+
+ [[ref child:@"a"] setValue:@1];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"a", @"null"];
+ return [added isEqualToArray:expected];
+ }];
+
+ [added removeAllObjects];
+
+ [[ref child:@"c"] setValue:@3];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"c", @"a"];
+ return [added isEqualToArray:expected];
+ }];
+
+ [added removeAllObjects];
+
+ [[ref child:@"b"] setValue:@2];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"b", @"null"];
+ return [added isEqualToArray:expected];
+ }];
+
+ [added removeAllObjects];
+
+ [[ref child:@"d"] setValue:@3];
+ [self waitUntil:^BOOL{
+ NSArray* expected = @[@"d", @"c"];
+ return [added isEqualToArray:expected];
+ }];
+}
+
+// Dropping some of the server data tests here, around prevName. They don't really test anything new, and mostly don't even test server data
+
+- (void) testPrevNameWorksWithMoves {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* moved = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [moved addObject:snapshot.key];
+ if (prevName) {
+ [moved addObject:prevName];
+ } else {
+ [moved addObject:@"null"];
+ }
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @"a", @".priority": @10},
+ @"b": @{@".value": @"b", @".priority": @20},
+ @"c": @{@".value": @"c", @".priority": @30},
+ @"d": @{@".value": @"d", @".priority": @40}
+ }];
+
+ __block BOOL ready = NO;
+ [[ref child:@"c"] setPriority:@50 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"c", @"d"];
+ XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild");
+
+ [moved removeAllObjects];
+ ready = NO;
+ [[ref child:@"c"] setPriority:@35 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[@"c", @"null"];
+ XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild");
+
+ [moved removeAllObjects];
+ ready = NO;
+ [[ref child:@"b"] setPriority:@33 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ expected = @[];
+ XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild to be empty");
+}
+
+- (void) testLocalEvents {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *eventString = [NSString stringWithFormat:@"%@ added", [snapshot value]];
+ [events addObject:eventString];
+ }];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *eventString = [NSString stringWithFormat:@"%@ removed", [snapshot value]];
+ [events addObject:eventString];
+ }];
+
+ __block BOOL ready = NO;
+ for (int i = 0; i < 5; ++i) {
+ [[ref childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ if (i == 4) {
+ ready = YES;
+ }
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"0 added", @"1 added", @"0 removed", @"2 added", @"1 removed", @"3 added", @"2 removed", @"4 added"];
+ XCTAssertTrue([events isEqualToArray:expected], @"Expecting window to stay at two nodes");
+}
+
+- (void) testRemoteEvents {
+ FTupleFirebase* pair = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = pair.one;
+ FIRDatabaseReference * reader = pair.two;
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+
+ [[reader queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *eventString = [NSString stringWithFormat:@"%@ added", [snapshot value]];
+ [events addObject:eventString];
+ }];
+
+ [[reader queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ NSString *oldEventString = [NSString stringWithFormat:@"%@ added", [snapshot value]];
+ [events removeObject:oldEventString];
+ }];
+
+ for (int i = 0; i < 5; ++i) {
+ [[writer childByAutoId] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ NSArray* expected = @[@"3 added", @"4 added"];
+ [self waitUntil:^BOOL{
+ return [events isEqualToArray:expected];
+ }];
+}
+
+- (void) testLimitOnEmptyNodeFiresValue {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testFilteringToNullPriorities {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ // Note: cannot set nil in a dictionary, just leave out priority
+ [ref setValue:@{
+ @"a": @0,
+ @"b": @1,
+ @"c": @{@".priority": @2, @".value": @2},
+ @"d": @{@".priority": @3, @".value": @3},
+ @"e": @{@".priority": @"hi", @".value": @4}
+ }];
+
+ __block BOOL ready = NO;
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryEndingAtValue:nil] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"a" : @0, @"b" : @1};
+ NSDictionary *val = [snapshot value];
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Expected only null priority keys");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testNullPrioritiesIncludedInEndAt {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ // Note: cannot set nil in a dictionary, just leave out priority
+ [ref setValue:@{
+ @"a": @0,
+ @"b": @1,
+ @"c": @{@".priority": @2, @".value": @2},
+ @"d": @{@".priority": @3, @".value": @3},
+ @"e": @{@".priority": @"hi", @".value": @4}
+ }];
+
+ __block BOOL ready = NO;
+ [[[ref queryOrderedByPriority] queryEndingAtValue:@2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"a" : @0, @"b" : @1, @"c" : @2};
+ NSDictionary *val = [snapshot value];
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Expected up to priority 2");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (NSSet *) dumpListensForRef:(FIRDatabaseReference *)ref {
+ NSMutableSet* dumpPieces = [[NSMutableSet alloc] init];
+ NSDictionary* listens = [ref.repo dumpListens];
+
+ FPath* nodePath = ref.path;
+ [listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *spec, id obj, BOOL *stop) {
+ if ([nodePath contains:spec.path]) {
+ FPath *relative = [FPath relativePathFrom:nodePath to:spec.path];
+ [dumpPieces addObject:[[FQuerySpec alloc] initWithPath:relative params:spec.params]];
+ }
+ }];
+
+ return dumpPieces;
+}
+
+- (NSSet *) expectDefaultListenerAtPath:(FPath *)path {
+ return [self expectParams:[FQueryParams defaultInstance] atPath:path];
+}
+
+- (NSSet *) expectParamssetValue:(NSSet *)paramsSet atPath:(FPath *)path {
+ NSMutableSet *all = [NSMutableSet set];
+ [paramsSet enumerateObjectsUsingBlock:^(FQueryParams *params, BOOL *stop) {
+ [all addObject:[[FQuerySpec alloc] initWithPath:path params:params]];
+ }];
+ return all;
+}
+
+- (NSSet *) expectParams:(FQueryParams *)params atPath:(FPath *)path {
+ return [self expectParamssetValue:[NSSet setWithObject:params] atPath:path];
+}
+
+- (void) testDedupesListensOnChild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ __block BOOL ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"a")]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected child listener");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"")]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"a")]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Child listener should be back");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testDedupeListensOnGrandchild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens;
+ __block BOOL ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected one listener");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+
+ [[ref child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener to override");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+
+ [ref removeAllObservers];
+ [[ref child:@"a/aa"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+ WAIT_FOR(ready);
+}
+
+- (void) testListenOnGrandparentOfTwoChildren {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ [[ref child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ __block BOOL ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected grandchild");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a/bb"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expecteda = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]];
+ NSSet* expectedb = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:expecteda];
+ [expected unionSet:expectedb];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected two grandchildren");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener to override");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expecteda = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]];
+ NSSet* expectedb = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:expecteda];
+ [expected unionSet:expectedb];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected grandchild listeners to return");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a/aa"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Expected one listener");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [[ref child:@"a/bb"] removeAllObservers];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testDedupingMultipleListenQueries {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ __block BOOL ready = NO;
+ FIRDatabaseQuery * aLim1 = [[ref child:@"a"] queryLimitedToLast:1];
+ FIRDatabaseHandle handle1 = [aLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Single query");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * rootLim1 = [ref queryLimitedToLast:1];
+ FIRDatabaseHandle handle2 = [rootLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* rootExpected = [self expectParams:expectedParams atPath:[FPath empty]];
+ NSSet* childExpected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:rootExpected];
+ [expected unionSet:childExpected];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Two queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * aLim5 = [[ref child:@"a"] queryLimitedToLast:5];
+ FIRDatabaseHandle handle3 = [aLim5 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams1 = [[FQueryParams alloc] init];
+ expectedParams1 = [expectedParams1 limitTo:1];
+ NSSet* rootExpected = [self expectParams:expectedParams1 atPath:[FPath empty]];
+
+ FQueryParams* expectedParams2 = [[FQueryParams alloc] init];
+ expectedParams2 = [expectedParams2 limitTo:5];
+ NSSet* childExpected = [self expectParamssetValue:[NSSet setWithObjects:expectedParams1, expectedParams2, nil] atPath:[FPath pathWithString:@"/a"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:childExpected];
+ [expected unionSet:rootExpected];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Three queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeObserverWithHandle:handle2];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams1 = [[FQueryParams alloc] init];
+ expectedParams1 = [expectedParams1 limitTo:1];
+ FQueryParams* expectedParams2 = [[FQueryParams alloc] init];
+ expectedParams2= [expectedParams2 limitTo:5];
+ NSSet* expected = [self expectParamssetValue:[NSSet setWithObjects:expectedParams1, expectedParams2, nil] atPath:[FPath pathWithString:@"/a"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Two queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [aLim1 removeObserverWithHandle:handle1];
+ [aLim5 removeObserverWithHandle:handle3];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testListenOnParentOfQueriedChildren {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block NSSet* listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No Listens yet");
+
+ __block BOOL ready = NO;
+ FIRDatabaseQuery * aLim1 = [[ref child:@"a"] queryLimitedToLast:1];
+ FIRDatabaseHandle handle1 = [aLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Single query");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * bLim1 = [[ref child:@"b"] queryLimitedToLast:1];
+ FIRDatabaseHandle handle2 = [bLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expecteda = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]];
+ NSSet* expectedb = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/b"]];
+ NSMutableSet* expected = [NSMutableSet setWithSet:expecteda];
+ [expected unionSet:expectedb];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Two queries");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseHandle handle3 = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Parent should override");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ // remove in slightly random order
+ [aLim1 removeObserverWithHandle:handle1];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Parent should override");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [ref removeObserverWithHandle:handle3];
+ ready = NO;
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ FQueryParams* expectedParams = [[FQueryParams alloc] init];
+ expectedParams = [expectedParams limitTo:1];
+ NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/b"]];
+ XCTAssertTrue([expected isEqualToSet:listens], @"Single query");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [bLim1 removeObserverWithHandle:handle2];
+ dispatch_async([FIRDatabaseQuery sharedQueue], ^{
+ listens = [self dumpListensForRef:ref];
+ XCTAssertTrue(listens.count == 0, @"No more listeners");
+ ready = YES;
+ });
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+-(void) testLimitWithMixOfNullAndNonNullPriorities {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* children = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:5] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [children addObject:[snapshot key]];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{
+ @"Vikrum": @{@".priority": @1000, @"score": @1000, @"name": @"Vikrum"},
+ @"Mike": @{@".priority": @500, @"score": @500, @"name": @"Mike"},
+ @"Andrew": @{@".priority": @50, @"score": @50, @"name": @"Andrew"},
+ @"James": @{@".priority": @7, @"score": @7, @"name": @"James"},
+ @"Sally": @{@".priority": @-7, @"score": @-7, @"name": @"Sally"},
+ @"Fred": @{@"score": @0, @"name": @"Fred"}
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"Sally", @"James", @"Andrew", @"Mike", @"Vikrum"];
+ XCTAssertTrue([children isEqualToArray:expected], @"Null priority should be left out");
+
+}
+
+-(void) testLimitWithMixOfNullAndNonNullPrioritiesOnServerData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{
+ @"Vikrum": @{@".priority": @1000, @"score": @1000, @"name": @"Vikrum"},
+ @"Mike": @{@".priority": @500, @"score": @500, @"name": @"Mike"},
+ @"Andrew": @{@".priority": @50, @"score": @50, @"name": @"Andrew"},
+ @"James": @{@".priority": @7, @"score": @7, @"name": @"James"},
+ @"Sally": @{@".priority": @-7, @"score": @-7, @"name": @"Sally"},
+ @"Fred": @{@"score": @0, @"name": @"Fred"}
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block int count = 0;
+ NSMutableArray* children = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:5] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [children addObject:[snapshot key]];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 5;
+ }];
+
+
+ NSArray* expected = @[@"Sally", @"James", @"Andrew", @"Mike", @"Vikrum"];
+ XCTAssertTrue([children isEqualToArray:expected], @"Null priority should be left out");
+
+}
+
+// Skipping context tests. Context is not implemented on iOS
+
+/* DISABLING for now, since I'm not 100% sure what the right behavior is.
+ Perhaps a merge at /foo should shadow server updates at /foo instead of
+ just the modified children? Not sure.
+- (void) testHandleUpdateThatDeletesEntireWindow {
+ Firebase* ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* snaps = [[NSMutableArray alloc] init];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val == nil) {
+ [snaps addObject:[NSNull null]];
+ } else {
+ [snaps addObject:val];
+ }
+ }];
+
+ NSDictionary* toSet = @{
+ @"a": @{@".priority": @1, @".value": @1},
+ @"b": @{@".priority": @2, @".value": @2},
+ @"c": @{@".priority": @3, @".value": @3}
+ };
+
+ [ref setValue:toSet];
+
+ __block BOOL ready = NO;
+ toSet = @{@"b": [NSNull null], @"c": [NSNull null]};
+ [ref updateChildValues:toSet withCompletionBlock:^(NSError* err, Firebase* ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@{@"b": @2, @"c": @3}, [NSNull null], @{@"a": @1}];
+ STAssertTrue([snaps isEqualToArray:expected], @"Expected %@ to equal %@", snaps, expected);
+}
+*/
+
+- (void) testHandlesAnOutOfViewQueryOnAChild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSDictionary* parent = nil;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ parent = [snapshot value];
+ }];
+
+ __block NSNumber* child = nil;
+ [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ child = [snapshot value];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @2};
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* parentExpected = @{@"b": @2};
+ NSNumber* childExpected = [NSNumber numberWithInt:1];
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+
+ ready = NO;
+ [ref updateChildValues:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ parentExpected = @{@"c": @3};
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+}
+
+- (void) testHandlesAChildQueryGoingOutOfViewOfTheParent {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSDictionary* parent = nil;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ parent = [snapshot value];
+ }];
+
+ __block NSNumber* child = nil;
+ [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ child = [snapshot value];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{@"a": @1};
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ NSDictionary* parentExpected = @{@"a": @1};
+ NSNumber* childExpected = [NSNumber numberWithInt:1];
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+
+ ready = NO;
+ [[ref child:@"b"] setValue:@2 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ parentExpected = @{@"b": @2};
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ parentExpected = @{@"a": @1};
+ XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element");
+ XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a");
+}
+
+- (void) testHandlesDivergingViews {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSDictionary* cVal = nil;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"c"] queryLimitedToLast:1];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ cVal = [snapshot value];
+ }];
+
+ __block NSDictionary* dVal = nil;
+ query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"d"] queryLimitedToLast:1];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ dVal = [snapshot value];
+ }];
+
+ __block BOOL ready = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3};
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* expected = @{@"c": @3};
+ XCTAssertTrue([cVal isEqualToDictionary:expected], @"should be c");
+ XCTAssertTrue([dVal isEqualToDictionary:expected], @"should be c");
+
+ ready = NO;
+ [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([cVal isEqualToDictionary:expected], @"should be c");
+ expected = @{@"d": @4};
+ XCTAssertTrue([dVal isEqualToDictionary:expected], @"should be d");
+}
+
+- (void) testHandlesRemovingAQueriedElement {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSNumber* val = nil;
+ [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ id newVal = [snapshot value];
+ if (newVal != nil) {
+ val = [snapshot value];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@2], @"Expected last element in window");
+
+ ready = NO;
+ [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@1], @"Should now be the next element in the window");
+}
+
+- (void) testStartAtAndLimit1Works {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSNumber* val = nil;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ id newVal = [snapshot value];
+ if (newVal != nil) {
+ val = [snapshot value];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@1], @"Expected first element in window");
+}
+
+// See case 1664
+- (void) testStartAtAndLimit1AndRemoveFirstChild {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block NSNumber* val = nil;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1];
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ id newVal = [snapshot value];
+ if (newVal != nil) {
+ val = [snapshot value];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@1], @"Expected first element in window");
+
+ ready = NO;
+ [[ref child:@"a"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([val isEqualToNumber:@2], @"Expected next element in window");
+}
+
+// See case 1169
+- (void) testStartAtWithTwoArgumentsWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ NSMutableArray* children = [[NSMutableArray alloc] init];
+
+ NSDictionary* toSet = @{
+ @"Walker": @{@"name": @"Walker", @"score": @20, @".priority": @20},
+ @"Michael": @{@"name": @"Michael", @"score": @100, @".priority": @100}
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@20 childKey:@"Walker"] queryLimitedToFirst:2];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSArray* expected = @[@"Walker", @"Michael"];
+ XCTAssertTrue([children isEqualToArray:expected], @"Expected both children");
+}
+
+- (void) testHandlesMultipleQueriesOnSameNode {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{
+ @"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5, @"f": @6
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ __block BOOL called = NO;
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // we got the initial data
+ XCTAssertFalse(called, @"This should only get called once, we don't update data after this");
+ called = YES;
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ __block NSDictionary* snap = nil;
+ // now do nested once calls
+ [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ snap = [snapshot value];
+ ready = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* expected = @{@"f": @6};
+ XCTAssertTrue([snap isEqualToDictionary:expected], @"Expected the correct data");
+}
+
+- (void) testHandlesOnceCalledOnNodeWithDefaultListener {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{
+ @"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5, @"f": @6
+ };
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // we got the initial data
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ __block NSNumber* snap = nil;
+ [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = [snapshot value];
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue([snap isEqualToNumber:@6], @"Got once response");
+}
+
+- (void) testHandlesOnceCalledOnNodeWithDefaultListenerAndNonCompleteLimit {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3};
+
+ [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ // do first listen
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+
+ __block NSDictionary* snap = nil;
+ [[ref queryLimitedToLast:5] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = [snapshot value];
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ NSDictionary* expected = @{@"a": @1, @"b": @2, @"c": @3};
+ XCTAssertTrue([snap isEqualToDictionary:expected], @"Got once response");
+}
+
+- (void) testRemoveTriggersRemoteEvents {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = tuple.one;
+ FIRDatabaseReference * reader = tuple.two;
+
+ __block BOOL ready = NO;
+
+ NSDictionary* toSet = @{@"a": @"a", @"b": @"b", @"c": @"c", @"d": @"d", @"e": @"e"};
+
+ [writer setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ __block int count = 0;
+
+ [[reader queryLimitedToLast:5] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"a" : @"a", @"b" : @"b", @"c" : @"c", @"d" : @"d", @"e" : @"e"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"First callback, expect all the data");
+ [[writer child:@"c"] removeValue];
+ } else {
+ XCTAssertTrue(count == 2, @"Should only get called twice");
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"a" : @"a", @"b" : @"b", @"d" : @"d", @"e" : @"e"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Second callback, expect all the remaining data");
+ ready = YES;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testEndingAtNameReturnsCorrectChildren {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSDictionary* toSet = @{
+ @"a": @"a",
+ @"b": @"b",
+ @"c": @"c",
+ @"d": @"d",
+ @"e": @"e",
+ @"f": @"f",
+ @"g": @"g",
+ @"h": @"h"
+ };
+
+ [self waitForCompletionOf:ref setValue:toSet];
+
+ __block NSDictionary* snap = nil;
+ __block BOOL done = NO;
+ FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"f"] queryLimitedToLast:5];
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = [snapshot value];
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ NSDictionary* expected = @{
+ @"b": @"b",
+ @"c": @"c",
+ @"d": @"d",
+ @"e": @"e",
+ @"f": @"f"
+ };
+ XCTAssertTrue([snap isEqualToDictionary:expected], @"Expected 5 elements, ending at f");
+}
+
+- (void) testListenForChildAddedWithLimitEnsureEventsFireProperly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+
+ NSDictionary* toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 0;
+ [[reader queryLimitedToLast:3] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child");
+ XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value");
+ } else if (count == 2) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child");
+ XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value");
+ } else if (count == 3) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child");
+ NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object");
+ } else {
+ XCTFail(@"wrong event count");
+ }
+ }];
+
+ WAIT_FOR(count == 3);
+}
+
+
+- (void) testListenForChildChangedWithLimitEnsureEventsFireProperly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+
+ NSDictionary* toSet = @{@"a": @"something", @"b": @"we'll", @"c": @"overwrite"};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 0;
+ [reader observeEventType:FIRDataEventTypeChildChanged withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child");
+ XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value");
+ } else if (count == 2) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child");
+ XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value");
+ } else if (count == 3) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child");
+ NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object");
+ } else {
+ XCTFail(@"wrong event count");
+ }
+ }];
+ toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}};
+ [writer setValue:toSet];
+
+ WAIT_FOR(count == 3);
+}
+
+- (void) testListenForChildRemovedWithLimitEnsureEventsFireProperly {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+
+ NSDictionary* toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 0;
+ [reader observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child");
+ XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value");
+ } else if (count == 2) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child");
+ XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value");
+ } else if (count == 3) {
+ XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child");
+ NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object");
+ } else {
+ XCTFail(@"wrong event count");
+ }
+ }];
+
+ done = NO;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // Load the data first
+ done = snapshot.value != [NSNull null] && [snapshot.value isEqualToDictionary:toSet];
+ }];
+
+ WAIT_FOR(done);
+
+ // Now do the removes
+ [[writer child:@"a"] removeValue];
+ [[writer child:@"b"] removeValue];
+ [[writer child:@"c"] removeValue];
+
+ WAIT_FOR(count == 3);
+}
+
+- (void) testQueriesBehaveProperlyAfterOnceCall {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = refs.one;
+ FIRDatabaseReference * reader = refs.two;
+
+ __block BOOL done = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3, @"d": @4};
+ [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ done = NO;
+ [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ // Ok, now do some queries
+ __block int startCount = 0;
+ __block int defaultCount = 0;
+ [[[reader queryOrderedByPriority] queryStartingAtValue:nil childKey:@"d"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ startCount++;
+ }];
+
+ [reader observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ defaultCount++;
+ }];
+
+ [reader observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTFail(@"Should not remove any children");
+ }];
+
+ WAIT_FOR(startCount == 1 && defaultCount == 4);
+}
+
+- (void) testIntegerKeysBehaveNumerically1 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES };
+ __block BOOL done = NO;
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"80"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"80" : @YES, @"550" : @YES, @"600" : @YES};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testIntegerKeysBehaveNumerically2 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES };
+ __block BOOL done = NO;
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"50"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"1" : @YES, @"6" : @YES, @"8" : @YES, @"50" : @YES};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testIntegerKeysBehaveNumerically3 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES };
+ __block BOOL done = NO;
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"50"] queryEndingAtValue:nil childKey:@"80"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *expected = @{@"50" : @YES, @"70" : @YES, @"80" : @YES};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testItemsPulledIntoLimitCorrectly {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSMutableArray* snaps = [[NSMutableArray alloc] init];
+
+ // Just so everything is cached locally.
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+
+ }];
+
+ [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ [snaps addObject:val];
+ }];
+
+ [ref setValue:@{
+ @"a": @{@".value": @1, @".priority": @1},
+ @"b": @{@".value": @2, @".priority": @2},
+ @"c": @{@".value": @3, @".priority": @3}
+ }];
+
+ __block BOOL ready = NO;
+ [[ref child:@"b"] setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ ready = YES;
+ }];
+
+ WAIT_FOR(ready);
+
+ NSArray* expected = @[@{@"b": @2, @"c": @3}, @{@"a": @1, @"c": @3}];
+ XCTAssertEqualObjects(snaps, expected, @"Incorrect snapshots.");
+}
+
+- (void)testChildChangedCausesChildRemovedEvent
+{
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ [[ref child:@"l/a"] setValue:@"1" andPriority:@"a"];
+ [[ref child:@"l/b"] setValue:@"2" andPriority:@"b"];
+ FIRDatabaseQuery *query = [[[[ref child:@"l"] queryOrderedByPriority] queryStartingAtValue:@"b"] queryEndingAtValue:@"d"];
+ __block BOOL removed = NO;
+ [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqualObjects(snapshot.value, @"2", @"Incorrect snapshot");
+ removed = YES;
+ }];
+
+ [[ref child:@"l/b"] setValue:@"4" andPriority:@"a"];
+
+ WAIT_FOR(removed);
+}
+
+- (void) testQueryHasRef {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ FIRDatabaseQuery *query = [ref queryOrderedByKey];
+ XCTAssertEqualObjects([query.ref path], [ref path], @"Should have same path");
+}
+
+- (void) testQuerySnapshotChildrenRespectDefaultOrdering {
+ FTupleFirebase* pair = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * writer = pair.one;
+ FIRDatabaseReference * reader = pair.two;
+ __block BOOL done = NO;
+
+ NSDictionary* list = @{
+ @"a": @{
+ @"thisvaluefirst": @{ @".value": @true, @".priority": @1 },
+ @"name": @{ @".value": @"Michael", @".priority": @2 },
+ @"thisvaluelast": @{ @".value": @true, @".priority": @3 },
+ },
+ @"b": @{
+ @"thisvaluefirst": @{ @".value": @true },
+ @"name": @{ @".value": @"Rob", @".priority": @2 },
+ @"thisvaluelast": @{ @".value": @true, @".priority": @3 },
+ },
+ @"c": @{
+ @"thisvaluefirst": @{ @".value": @true, @".priority": @1 },
+ @"name": @{ @".value": @"Jonny", @".priority": @2 },
+ @"thisvaluelast": @{ @".value": @true, @".priority": @"somestring" },
+ }
+ };
+
+ [writer setValue:list withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+ [[reader queryOrderedByChild:@"name"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSArray *expectedKeys = @[@"thisvaluefirst", @"name", @"thisvaluelast"];
+ NSArray *expectedNames = @[@"Jonny", @"Michael", @"Rob"];
+
+ // Validate that snap.child() resets order to default for child snaps
+ NSMutableArray *orderedKeys = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot *childSnap in [snapshot childSnapshotForPath:@"b"].children) {
+ [orderedKeys addObject:childSnap.key];
+ }
+ XCTAssertEqualObjects(expectedKeys, orderedKeys, @"Should have matching ordered lists of keys");
+
+ // Validate that snap.forEach() resets ordering to default for child snaps
+ NSMutableArray *orderedNames = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot *childSnap in snapshot.children) {
+ [orderedNames addObject:[childSnap childSnapshotForPath:@"name"].value];
+ [orderedKeys removeAllObjects];
+ for (FIRDataSnapshot *grandchildSnap in childSnap.children) {
+ [orderedKeys addObject:grandchildSnap.key];
+ }
+ XCTAssertEqualObjects(expectedKeys, orderedKeys, @"Should have matching ordered lists of keys");
+ }
+ XCTAssertEqualObjects(expectedNames, orderedNames, @"Should have matching ordered lists of names");
+
+ done = YES;
+ }];
+ WAIT_FOR(done);
+}
+
+- (void) testAddingListensForTheSamePathDoesNotCheckFail {
+ // This bug manifests itself if there's a hierarchy of query listener, default listener and one-time listener
+ // underneath.
+ // In Java implementation, during one-time listener registration, sync-tree traversal stopped as soon as it found
+ // a complete server cache (this is the case for not indexed query view). The problem is that the same traversal was
+ // looking for a ancestor default view, and the early exit prevented from finding the default listener above the
+ // one-time listener. Event removal code path wasn't removing the listener because it stopped as soon as it
+ // found the default view. This left the zombie one-time listener and check failed on the second attempt to
+ // create a listener for the same path (asana#61028598952586).
+
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+
+ [[ref child:@"child"] setValue:@{@"name": @"John"}];
+ [[[ref queryOrderedByChild:@"name"] queryEqualToValue:@"John"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+ [[[ref child:@"child"] child:@"favoriteToy"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+ [[[ref child:@"child"] child:@"favoriteToy"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FIRDatabaseTests.m b/Example/Database/Tests/Integration/FIRDatabaseTests.m
new file mode 100644
index 0000000..8a5742d
--- /dev/null
+++ b/Example/Database/Tests/Integration/FIRDatabaseTests.m
@@ -0,0 +1,375 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FIRApp.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FIRDatabase.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FIROptions.h"
+#import "FTestHelpers.h"
+#import "FMockStorageEngine.h"
+#import "FTestBase.h"
+#import "FTestHelpers.h"
+#import "FIRFakeApp.h"
+
+@interface FIRDatabaseTests : FTestBase
+
+@end
+
+static const NSInteger kFErrorCodeWriteCanceled = 3;
+
+@implementation FIRDatabaseTests
+
+- (void) testFIRDatabaseForNilApp {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
+ XCTAssertThrowsSpecificNamed([FIRDatabase databaseForApp:nil], NSException, @"InvalidFIRApp");
+#pragma clang diagnostic pop
+}
+
+- (void) testDatabaseForApp {
+ FIRDatabase *database = [self databaseForURL:self.databaseURL];
+ XCTAssertEqualObjects(self.databaseURL, [database reference].URL);
+}
+
+- (void) testDatabaseForAppWithInvalidURLs {
+ XCTAssertThrows([self databaseForURL:nil]);
+ XCTAssertThrows([self databaseForURL:@"not-a-url"]);
+ XCTAssertThrows([self databaseForURL:@"http://x.example.com/paths/are/bad"]);
+}
+
+- (void) testReferenceWithPath {
+ FIRDatabase *db = [self defaultDatabase];
+ NSString *expectedURL = [NSString stringWithFormat:@"%@/foo", self.databaseURL];
+ XCTAssertEqualObjects(expectedURL, [db referenceWithPath:@"foo"].URL);
+}
+
+- (void) testReferenceFromURLWithEmptyPath {
+ FIRDatabaseReference *ref = [[self defaultDatabase] referenceFromURL:self.databaseURL];
+ XCTAssertEqualObjects(self.databaseURL, ref.URL);
+}
+
+- (void) testReferenceFromURLWithPath {
+ NSString *url = [NSString stringWithFormat:@"%@/foo/bar", self.databaseURL];
+ FIRDatabaseReference *ref = [[self defaultDatabase] referenceFromURL:url];
+ XCTAssertEqualObjects(url, ref.URL);
+}
+
+- (void) testReferenceFromURLWithWrongURL {
+ NSString *url = [NSString stringWithFormat:@"%@/foo/bar", @"https://foobar.firebaseio.com"];
+ XCTAssertThrows([[self defaultDatabase] referenceFromURL:url]);
+}
+
+- (void) testReferenceEqualityForFIRDatabase {
+ FIRDatabase *db1 = [self databaseForURL:self.databaseURL name:@"db1"];
+ FIRDatabase *db2 = [self databaseForURL:self.databaseURL name:@"db2"];
+ FIRDatabase *altDb = [self databaseForURL:self.databaseURL name:@"altDb"];
+ FIRDatabase *wrongHostDb = [self databaseForURL:@"http://tests.example.com"];
+
+ FIRDatabaseReference *testRef1 = [db1 reference];
+ FIRDatabaseReference *testRef2 = [db1 referenceWithPath:@"foo"];
+ FIRDatabaseReference *testRef3 = [altDb reference];
+ FIRDatabaseReference *testRef4 = [wrongHostDb reference];
+ FIRDatabaseReference *testRef5 = [db2 reference];
+ FIRDatabaseReference *testRef6 = [db2 reference];
+
+ // Referential equality
+ XCTAssertTrue(testRef1.database == testRef2.database);
+ XCTAssertFalse(testRef1.database == testRef3.database);
+ XCTAssertFalse(testRef1.database == testRef4.database);
+ XCTAssertFalse(testRef1.database == testRef5.database);
+ XCTAssertFalse(testRef1.database == testRef6.database);
+
+ // references from same FIRDatabase same identical .database references.
+ XCTAssertTrue(testRef5.database == testRef6.database);
+
+ [db1 goOffline];
+ [db2 goOffline];
+ [altDb goOffline];
+ [wrongHostDb goOffline];
+}
+
+- (FIRDatabaseReference *)rootRefWithEngine:(id<FStorageEngine>)engine name:(NSString *)name {
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:name];
+ config.persistenceEnabled = YES;
+ config.forceStorageEngine = engine;
+ return [[FIRDatabaseReference alloc] initWithConfig:config];
+}
+
+- (void) testPurgeWritesPurgesAllWrites {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesPurgesAllWrites"];
+ FIRDatabase *database = ref.database;
+
+ [database goOffline];
+
+ [[ref childByAutoId] setValue:@"test-value-1"];
+ [[ref childByAutoId] setValue:@"test-value-2"];
+ [[ref childByAutoId] setValue:@"test-value-3"];
+ [[ref childByAutoId] setValue:@"test-value-4"];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)4);
+
+ [database purgeOutstandingWrites];
+ [self waitForEvents:ref];
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)0);
+
+ [database goOnline];
+}
+
+- (void) testPurgeWritesAreCanceledInOrder {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesAndCanceledInOrder"];
+ FIRDatabase *database = ref.database;
+
+ [database goOffline];
+
+ NSMutableArray *order = [NSMutableArray array];
+
+ [[ref childByAutoId] setValue:@"test-value-1" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"1"];
+ }];
+ [[ref childByAutoId] setValue:@"test-value-2" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"2"];
+ }];
+ [[ref childByAutoId] setValue:@"test-value-3" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"3"];
+ }];
+ [[ref childByAutoId] setValue:@"test-value-4" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [order addObject:@"4"];
+ }];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)4);
+
+ [database purgeOutstandingWrites];
+ [self waitForEvents:ref];
+ XCTAssertEqual(engine.userWrites.count, (NSUInteger)0);
+ XCTAssertEqualObjects(order, (@[@"1", @"2", @"3", @"4"]));
+
+ [database goOnline];
+}
+
+- (void)testPurgeWritesCancelsOnDisconnects {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesCancelsOnDisconnects"];
+ FIRDatabase *database = ref.database;
+
+ [database goOffline];
+
+ NSMutableArray *events = [NSMutableArray array];
+
+ [[ref childByAutoId] onDisconnectSetValue:@"test-value-1" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"1"];
+ }];
+
+ [[ref childByAutoId] onDisconnectSetValue:@"test-value-2" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"2"];
+ }];
+
+ [self waitForEvents:ref];
+
+ [database purgeOutstandingWrites];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqualObjects(events, (@[@"1", @"2"]));
+}
+
+- (void) testPurgeWritesReraisesEvents {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [[self rootRefWithEngine:engine name:@"purgeWritesReraiseEvents"] childByAutoId];
+ FIRDatabase *database = ref.database;
+
+ [self waitForCompletionOf:ref setValue:@{@"foo": @"foo-value", @"bar": @{@"qux": @"qux-value"}}];
+
+ NSMutableArray *fooValues = [NSMutableArray array];
+ NSMutableArray *barQuuValues = [NSMutableArray array];
+ NSMutableArray *barQuxValues = [NSMutableArray array];
+ NSMutableArray *cancelOrder = [NSMutableArray array];
+
+ [[ref child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [fooValues addObject:snapshot.value];
+ }];
+ [[ref child:@"bar/quu"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [barQuuValues addObject:snapshot.value];
+ }];
+ [[ref child:@"bar/qux"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [barQuxValues addObject:snapshot.value];
+ }];
+
+ [database goOffline];
+
+ [[ref child:@"foo"] setValue:@"new-foo-value" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ // This should be after we raised events
+ XCTAssertEqualObjects(fooValues.lastObject, @"foo-value");
+ [cancelOrder addObject:@"foo-1"];
+ }];
+
+ [[ref child:@"bar"] updateChildValues:@{@"quu": @"quu-value", @"qux": @"new-qux-value"}
+ withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ // This should be after we raised events
+ XCTAssertEqualObjects(barQuxValues.lastObject, @"qux-value");
+ XCTAssertEqualObjects(barQuuValues.lastObject, [NSNull null]);
+ [cancelOrder addObject:@"bar"];
+ }];
+
+ [[ref child:@"foo"] setValue:@"newest-foo-value" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ // This should be after we raised events
+ XCTAssertEqualObjects(fooValues.lastObject, @"foo-value");
+ [cancelOrder addObject:@"foo-2"];
+ }];
+
+ [database purgeOutstandingWrites];
+
+ [self waitForEvents:ref];
+
+ XCTAssertEqualObjects(cancelOrder, (@[@"foo-1", @"bar", @"foo-2"]));
+ XCTAssertEqualObjects(fooValues, (@[@"foo-value", @"new-foo-value", @"newest-foo-value", @"foo-value"]));
+ XCTAssertEqualObjects(barQuuValues, (@[[NSNull null], @"quu-value", [NSNull null]]));
+ XCTAssertEqualObjects(barQuxValues, (@[@"qux-value", @"new-qux-value", @"qux-value"]));
+
+ [database goOnline];
+ // Make sure we're back online and reconnected again
+ [self waitForRoundTrip:ref];
+
+ // No events should be reraised
+ XCTAssertEqualObjects(cancelOrder, (@[@"foo-1", @"bar", @"foo-2"]));
+ XCTAssertEqualObjects(fooValues, (@[@"foo-value", @"new-foo-value", @"newest-foo-value", @"foo-value"]));
+ XCTAssertEqualObjects(barQuuValues, (@[[NSNull null], @"quu-value", [NSNull null]]));
+ XCTAssertEqualObjects(barQuxValues, (@[@"qux-value", @"new-qux-value", @"qux-value"]));
+}
+
+- (void)testPurgeWritesCancelsTransactions {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FIRDatabaseReference *ref = [[self rootRefWithEngine:engine name:@"purgeWritesCancelsTransactions"] childByAutoId];
+ FIRDatabase *database = ref.database;
+
+ NSMutableArray *events = [NSMutableArray array];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [events addObject:[NSString stringWithFormat:@"value-%@", snapshot.value]];
+ }];
+
+ // Make sure the first value event is fired
+ [self waitForRoundTrip:ref];
+
+ [database goOffline];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"1"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"cancel-1"];
+ }];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"2"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(error.code, kFErrorCodeWriteCanceled);
+ [events addObject:@"cancel-2"];
+ }];
+
+ [database purgeOutstandingWrites];
+
+ [self waitForEvents:ref];
+
+ // The order should really be cancel-1 then cancel-2, but meh, to difficult to implement currently...
+ XCTAssertEqualObjects(events, (@[@"value-<null>", @"value-1", @"value-2", @"value-<null>", @"cancel-2", @"cancel-1"]));
+}
+
+- (void) testPersistenceEnabled {
+ id app = [[FIRFakeApp alloc] initWithName:@"testPersistenceEnabled" URL:self.databaseURL];
+ FIRDatabase *database = [FIRDatabase databaseForApp:app];
+ database.persistenceEnabled = YES;
+ XCTAssertTrue(database.persistenceEnabled);
+
+ // Just do a dummy observe that should get null added to the persistent cache.
+ FIRDatabaseReference *ref = [[database reference] childByAutoId];
+ [self waitForValueOf:ref toBe:[NSNull null]];
+
+ // Now go offline and since null is cached offline, our observer should still complete.
+ [database goOffline];
+ [self waitForValueOf:ref toBe:[NSNull null]];
+}
+
+- (void) testPersistenceCacheSizeBytes {
+ id app = [[FIRFakeApp alloc] initWithName:@"testPersistenceCacheSizeBytes" URL:self.databaseURL];
+ FIRDatabase *database = [FIRDatabase databaseForApp:app];
+ database.persistenceEnabled = YES;
+
+ int oneMegabyte = 1 * 1024 * 1024;
+
+ XCTAssertThrows([database setPersistenceCacheSizeBytes: 1], @"Cache must be a least 1 MB.");
+ XCTAssertThrows([database setPersistenceCacheSizeBytes: 101 * oneMegabyte],
+ @"Cache must be less than 100 MB.");
+ database.persistenceCacheSizeBytes = 2 * oneMegabyte;
+ XCTAssertEqual(2 * oneMegabyte, database.persistenceCacheSizeBytes);
+
+ [database reference]; // Initialize database.
+
+ XCTAssertThrows([database setPersistenceCacheSizeBytes: 3 * oneMegabyte],
+ @"Persistence can't be changed after initialization.");
+ XCTAssertEqual(2 * oneMegabyte, database.persistenceCacheSizeBytes);
+}
+
+- (void) testCallbackQueue {
+ id app = [[FIRFakeApp alloc] initWithName:@"testCallbackQueue" URL:self.databaseURL];
+ FIRDatabase *database = [FIRDatabase databaseForApp:app];
+ dispatch_queue_t callbackQueue = dispatch_queue_create("testCallbackQueue", NULL);
+ database.callbackQueue = callbackQueue;
+ XCTAssertEqual(callbackQueue, database.callbackQueue);
+
+ __block BOOL done = NO;
+ [database.reference.childByAutoId observeSingleEventOfType:FIRDataEventTypeValue
+ withBlock:^(FIRDataSnapshot *snapshot) {
+ dispatch_assert_queue(callbackQueue);
+ done = YES;
+ }];
+ WAIT_FOR(done);
+ [database goOffline];
+}
+
+- (FIRDatabase *) defaultDatabase {
+ return [self databaseForURL:self.databaseURL];
+}
+
+- (FIRDatabase *) databaseForURL:(NSString *)url {
+ NSString *name = [NSString stringWithFormat:@"url:%@", url];
+ return [self databaseForURL:url name:name];
+}
+
+- (FIRDatabase *) databaseForURL:(NSString *)url name:(NSString *)name {
+ id app = [[FIRFakeApp alloc] initWithName:name URL:url];
+ return [FIRDatabase databaseForApp:app];
+}
+@end
diff --git a/Example/Database/Tests/Integration/FKeepSyncedTest.m b/Example/Database/Tests/Integration/FKeepSyncedTest.m
new file mode 100644
index 0000000..96d5cf8
--- /dev/null
+++ b/Example/Database/Tests/Integration/FKeepSyncedTest.m
@@ -0,0 +1,230 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+#import "FTestHelpers.h"
+#import "FTestBase.h"
+
+@interface FKeepSyncedTest : FTestBase
+
+@end
+
+@implementation FKeepSyncedTest
+
+static NSUInteger fGlobalKeepSyncedTestCounter = 0;
+
+- (void)assertIsKeptSynced:(FIRDatabaseQuery *)query {
+ FIRDatabaseReference *ref = query.ref;
+
+ // First set a unique value to the value of child
+ fGlobalKeepSyncedTestCounter++;
+ NSNumber *currentValue = @(fGlobalKeepSyncedTestCounter);
+ __block BOOL done = NO;
+ [ref setValue:@{ @"child": currentValue} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertNil(error);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ // Next go offline, if it's kept synced we should have kept the value, after going offline no way to get the value
+ // except from cache
+ [FIRDatabaseReference goOffline];
+
+ [query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // We should receive an event
+ XCTAssertEqualObjects(snapshot.value, @{@"child" : currentValue});
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ // All good, go back online
+ [FIRDatabaseReference goOnline];
+}
+
+- (void)assertNotKeptSynced:(FIRDatabaseQuery *)query {
+ FIRDatabaseReference *ref = query.ref;
+
+ // First set a unique value to the value of child
+ fGlobalKeepSyncedTestCounter++;
+ NSNumber *currentValue = @(fGlobalKeepSyncedTestCounter);
+ fGlobalKeepSyncedTestCounter++;
+ NSNumber *newValue = @(fGlobalKeepSyncedTestCounter);
+ __block BOOL done = NO;
+ [ref setValue:@{ @"child": currentValue} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertNil(error);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ // Next go offline, if it's kept synced we should have kept the value, after going offline no way to get the value
+ // except from cache
+ [FIRDatabaseReference goOffline];
+
+ [query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // We should receive an event
+ XCTAssertEqualObjects(snapshot.value, @{@"child" : newValue});
+ done = YES;
+ }];
+
+ // By now, if we had it synced we should have gotten an event with the wrong value
+ // Write a new value so the value event listener will be triggered
+ [ref setValue:@{ @"child": newValue}];
+ WAIT_FOR(done);
+
+ // All good, go back online
+ [FIRDatabaseReference goOnline];
+}
+
+- (void)testKeepSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ [ref keepSynced:NO];
+ [self assertNotKeptSynced:ref];
+}
+
+- (void)testManyKeepSyncedCallsDontAccumulate {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [ref keepSynced:YES];
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ // If it were balanced, this would not be enough
+ [ref keepSynced:NO];
+ [ref keepSynced:NO];
+ [self assertNotKeptSynced:ref];
+
+ // If it were balanced, this would not be enough
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testRemoveAllObserversDoesNotAffectKeepSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ [ref removeAllObservers];
+ [self assertIsKeptSynced:ref];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testRemoveSingleObserverDoesNotAffectKeepSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ __block BOOL done = NO;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ [ref removeObserverWithHandle:handle];
+
+ [self assertIsKeptSynced:ref];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testKeepSyncedNoDoesNotAffectExistingObserver {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:ref];
+
+ __block BOOL done = NO;
+ FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = [snapshot.value isEqual:@"done"];
+ }];
+
+ // cleanup
+ [ref keepSynced:NO];
+
+ [ref setValue:@"done"];
+
+ WAIT_FOR(done);
+ [ref removeObserverWithHandle:handle];
+}
+
+
+- (void)testDifferentQueriesAreIndependent {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+ FIRDatabaseQuery *query1 = [ref queryLimitedToFirst:1];
+ FIRDatabaseQuery *query2 = [ref queryLimitedToFirst:2];
+
+ [query1 keepSynced:YES];
+ [self assertIsKeptSynced:query1];
+ [self assertNotKeptSynced:query2];
+
+ [query2 keepSynced:YES];
+ [self assertIsKeptSynced:query1];
+ [self assertIsKeptSynced:query2];
+
+ [query1 keepSynced:NO];
+ [self assertIsKeptSynced:query2];
+ [self assertNotKeptSynced:query1];
+
+ [query2 keepSynced:NO];
+ [self assertNotKeptSynced:query1];
+ [self assertNotKeptSynced:query2];
+
+}
+
+- (void)testChildIsKeptSynced {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence];
+ FIRDatabaseReference *child = [ref child:@"random-child"];
+
+ [ref keepSynced:YES];
+ [self assertIsKeptSynced:child];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+- (void)testRootIsKeptSynced {
+ FIRDatabaseReference *ref = [[FTestHelpers getRandomNodeWithoutPersistence] root];
+
+ [ref keepSynced:YES];
+ // Run on random child to make sure writes from this test doesn't interfere with any other tests.
+ [self assertIsKeptSynced:[ref childByAutoId]];
+
+ // cleanup
+ [ref keepSynced:NO];
+}
+
+// TODO[offline]: Cancel listens for keep synced....
+
+
+
+@end
diff --git a/Example/Database/Tests/Integration/FOrder.h b/Example/Database/Tests/Integration/FOrder.h
new file mode 100644
index 0000000..d39de2a
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrder.h
@@ -0,0 +1,22 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FOrder : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FOrder.m b/Example/Database/Tests/Integration/FOrder.m
new file mode 100644
index 0000000..e8c628b
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrder.m
@@ -0,0 +1,646 @@
+/*
+ * 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 "FOrder.h"
+#import "FIRDatabaseReference.h"
+#import "FTypedefs_Private.h"
+#import "FTupleFirebase.h"
+#import "FTestHelpers.h"
+#import "FEventTester.h"
+#import "FTupleEventTypeString.h"
+
+@implementation FOrder
+
+- (void) testPushEnumerateAndCheckCorrectOrder {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot * snapshot) {
+ int expected = 0;
+ for (FIRDataSnapshot * child in snapshot.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expects values match.");
+ expected = expected + 1;
+ }
+ XCTAssertTrue(expected == 10, @"Should get all of the children");
+ XCTAssertTrue(expected == snapshot.childrenCount, @"Snapshot should report correct count");
+ }];
+}
+
+- (void) testPushEnumerateManyPathsWriteAndCheckOrder {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ NSMutableArray* paths = [[NSMutableArray alloc] init];
+
+ for(int i = 0; i < 20; i++) {
+ [paths addObject:[node childByAutoId]];
+ }
+
+ for(int i = 0; i < 20; i++) {
+ [(FIRDatabaseReference *)[paths objectAtIndex:i] setValue:[NSNumber numberWithInt:i]];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ int expected = 0;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expects values match.");
+ expected = expected + 1;
+ }
+ XCTAssertTrue(expected == 20, @"Should get all of the children");
+ XCTAssertTrue(expected == snap.childrenCount, @"Snapshot should report correct count");
+ }];
+}
+
+- (void) testPushDataReconnectReadBackAndVerifyOrder {
+
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ __block int expected = 0;
+ __block int nodesSet = 0;
+ FIRDatabaseReference * node = tuple.one;
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) {
+ nodesSet++;
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return nodesSet == 10;
+ }];
+
+ __block BOOL done = NO;
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ expected = 0;
+ //[snap forEach:^BOOL(FIRDataSnapshot *child) {
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expected child value");
+ expected = expected + 1;
+ //return NO;
+ }
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ XCTAssertTrue(nodesSet == 10, @"All of the nodes have been set");
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ expected = 0;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expected child value");
+ expected = expected + 1;
+ }
+ done = YES;
+ XCTAssertTrue(expected == 10, @"Saw the expected number of children %d == 10", expected);
+ }];
+
+}
+
+- (void) testPushDataWithPrioritiesReconnectReadBackAndVerifyOrder {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ __block int expected = 0;
+ __block int nodesSet = 0;
+ FIRDatabaseReference * node = tuple.one;
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i] andPriority:[NSNumber numberWithInt:(10 - i)] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ nodesSet = nodesSet + 1;
+ }];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+
+ [self waitUntil:^BOOL{
+ return nodesSet == 10;
+ }];
+
+ XCTAssertTrue(nodesSet == 10, @"All of the nodes have been set");
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+}
+
+- (void) testPushDataWithExponentialPrioritiesReconnectReadBackAndVerifyOrder {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ __block int expected = 0;
+ __block int nodesSet = 0;
+ FIRDatabaseReference * node = tuple.one;
+ for(int i = 0; i < 10; i++) {
+ [[node childByAutoId] setValue:[NSNumber numberWithInt:i] andPriority:[NSNumber numberWithDouble:(111111111111111111111111111111.0 / pow(10, i))] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ nodesSet = nodesSet + 1;
+ }];
+ }
+
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+
+ WAIT_FOR(nodesSet == 10);
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ expected = 9;
+ for (FIRDataSnapshot * child in snap.children) {
+ XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority");
+ expected = expected - 1;
+ }
+ XCTAssertTrue(expected == -1, @"Saw the expected number of children");
+ }];
+}
+
+- (void) testThatNodesWithoutValuesAreNotEnumerated {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+ [node child:@"foo"];
+ [[node child:@"bar"] setValue:@"test"];
+
+ __block int items = 0;
+ [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) {
+
+ for (FIRDataSnapshot * child in snap.children) {
+ items = items + 1;
+ XCTAssertEqualObjects([child key], @"bar", @"Saw the child which had a value set and not the empty one");
+ }
+
+ XCTAssertTrue(items == 1, @"Saw only the one that was actually set.");
+ }];
+}
+
+- (void) testChildMovedEventWhenPriorityChanges {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+
+ NSArray* expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"c"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildMoved withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ [et waitForInitialization];
+
+ [[node child:@"a"] setValue:@"first" andPriority:@1];
+ [[node child:@"b"] setValue:@"second" andPriority:@2];
+ [[node child:@"c"] setValue:@"third" andPriority:@3];
+
+ [[node child:@"a"] setPriority:@15];
+
+ [et wait];
+}
+
+
+- (void) testCanResetPriorityToNull {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [[node child:@"a"] setValue:@"a" andPriority:@1];
+ [[node child:@"b"] setValue:@"b" andPriority:@2];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ NSArray* expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"a"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ [et wait];
+
+ expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildMoved withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"b"],
+ [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ [[node child:@"b"] setPriority:nil];
+
+ [et wait];
+
+ __block BOOL ready = NO;
+ [[node child:@"b"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertTrue([snapshot priority] == [NSNull null], @"Should be null");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testInsertingANodeUnderALeafPreservesItsPriority {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ snap = s;
+ }];
+
+ [node setValue:@"a" andPriority:@10];
+ [[node child:@"deeper"] setValue:@"deeper"];
+
+ [self waitUntil:^BOOL{
+ id result = [snap value];
+ NSDictionary* expected = @{@"deeper": @"deeper"};
+ return snap != nil && [result isKindOfClass:[NSDictionary class]] && [result isEqualToDictionary:expected];
+ }];
+
+ XCTAssertEqualObjects([snap priority], @10, @"Proper value");
+}
+
+- (void) testVerifyOrderOfMixedNumbersStringNoPriorities {
+ FTupleFirebase* tuple = [FTestHelpers getRandomNodePair];
+
+ NSArray* nodeAndPriorities = @[
+ @"alpha42", @"zed",
+ @"noPriorityC", [NSNull null],
+ @"num41", @500,
+ @"noPriorityB", [NSNull null],
+ @"num80", @4000.1,
+ @"num50", @4000,
+ @"num10", @24,
+ @"alpha41", @"zed",
+ @"alpha20", @"horse",
+ @"num20", @123,
+ @"num70", @4000.01,
+ @"noPriorityA", [NSNull null],
+ @"alpha30", @"tree",
+ @"num30", @300,
+ @"num60", @4000.001,
+ @"alpha10", @"0horse",
+ @"num42", @500,
+ @"alpha40", @"zed",
+ @"num40", @500
+ ];
+
+ __block int setsCompleted = 0;
+
+ for (int i = 0; i < [nodeAndPriorities count]; i++) {
+ FIRDatabaseReference * n = [tuple.one child:[nodeAndPriorities objectAtIndex:i++]];
+ [n setValue:@1 andPriority:[nodeAndPriorities objectAtIndex:i] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ setsCompleted = setsCompleted + 1;
+ }];
+ }
+
+ NSString* expected = @"noPriorityA, noPriorityB, noPriorityC, num10, num20, num30, num40, num41, num42, num50, num60, num70, num80, alpha10, alpha20, alpha30, alpha40, alpha41, alpha42, ";
+
+ [super snapWaiter:tuple.one withBlock:^(FIRDataSnapshot *snap) {
+ NSMutableString* output = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * n in snap.children) {
+ [output appendFormat:@"%@, ", [n key]];
+ }
+
+ XCTAssertTrue([expected isEqualToString:output], @"Proper order");
+ }];
+
+ WAIT_FOR(setsCompleted == [nodeAndPriorities count] / 2);
+
+ [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) {
+ NSMutableString* output = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * n in snap.children) {
+ [output appendFormat:@"%@, ", [n key]];
+ }
+
+ XCTAssertTrue([expected isEqualToString:output], @"Proper order");
+ }];
+}
+
+- (void) testVerifyOrderOfIntegerNames {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ NSArray* keys = @[
+ @"foo",
+ @"bar",
+ @"03",
+ @"0",
+ @"100",
+ @"20",
+ @"5",
+ @"3",
+ @"003",
+ @"9"
+ ];
+
+ __block int setsCompleted = 0;
+
+ for (int i = 0; i < [keys count]; i++) {
+ FIRDatabaseReference * n = [ref child:[keys objectAtIndex:i]];
+ [n setValue:@1 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ setsCompleted = setsCompleted + 1;
+ }];
+ }
+
+ NSString* expected = @"0, 3, 03, 003, 5, 9, 20, 100, bar, foo, ";
+
+ [super snapWaiter:ref withBlock:^(FIRDataSnapshot *snap) {
+ NSMutableString* output = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * n in snap.children) {
+ [output appendFormat:@"%@, ", [n key]];
+ }
+
+ XCTAssertTrue([expected isEqualToString:output], @"Proper order");
+ }];
+}
+
+- (void) testPrevNameIsCorrectOnChildAddedEvent {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"a": @1, @"b": @2, @"c": @3}];
+
+
+ NSMutableString* added = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) {
+ [added appendFormat:@"%@ %@, ", [snap key], prevName];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"a (null), b a, c b, "], @"Proper order and prevname");
+
+}
+
+- (void) testPrevNameIsCorrectWhenAddingNewNodes {
+
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"b": @2, @"c": @3, @"d": @4}];
+
+ NSMutableString* added = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) {
+ [added appendFormat:@"%@ %@, ", [snap key], prevName];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"b (null), c b, d c, "], @"Proper order and prevname");
+
+ [added setString:@""];
+ [[node child:@"a"] setValue:@1];
+ [self waitUntil:^BOOL{
+ return count == 4;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"a (null), "], @"Proper insertion of new node");
+
+ [added setString:@""];
+ [[node child:@"e"] setValue:@5];
+ [self waitUntil:^BOOL{
+ return count == 5;
+ }];
+ XCTAssertTrue([added isEqualToString:@"e d, "], @"Proper insertion of new node");
+}
+
+- (void) testPrevNameIsCorrectWhenAddingNewNodesWithJSON {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ [node setValue:@{@"b": @2, @"c": @3, @"d": @4}];
+
+ NSMutableString* added = [[NSMutableString alloc] init];
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) {
+ [added appendFormat:@"%@ %@, ", [snap key], prevName];
+ count++;
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"b (null), c b, d c, "], @"Proper order and prevname");
+
+ [added setString:@""];
+ [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}];
+ [self waitUntil:^BOOL{
+ return count == 4;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"a (null), "], @"Proper insertion of new node");
+
+ [added setString:@""];
+ [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5}];
+ [self waitUntil:^BOOL{
+ return count == 5;
+ }];
+
+ XCTAssertTrue([added isEqualToString:@"e d, "], @"Proper insertion of new node");
+}
+
+- (void) testPrevNameIsCorrectWhenMovingNodes {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableString* moved = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [moved appendFormat:@"%@ %@, ", snapshot.key, prevName];
+ count++;
+ }];
+
+ [[node child:@"a"] setValue:@"a" andPriority:@1];
+ [[node child:@"b"] setValue:@"a" andPriority:@2];
+ [[node child:@"c"] setValue:@"a" andPriority:@3];
+ [[node child:@"d"] setValue:@"a" andPriority:@4];
+
+ [[node child:@"d"] setPriority:@0];
+ [self waitUntil:^BOOL{
+ return count == 1;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"d (null), "], @"Got first move");
+
+ [moved setString:@""];
+ [[node child:@"a"] setPriority:@4];
+ [self waitUntil:^BOOL{
+ return count == 2;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"a c, "], @"Got second move");
+
+ [moved setString:@""];
+ [[node child:@"c"] setPriority:@0.5];
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"c d, "], @"Got third move");
+}
+
+
+- (void) testPrevNameIsCorrectWhenSettingWholeJsonDict {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ NSMutableString* moved = [[NSMutableString alloc] init];
+
+ __block int count = 0;
+ [node observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [moved appendFormat:@"%@ %@, ", snapshot.key, prevName];
+ count++;
+ }];
+
+ [node setValue:@{
+ @"a": @{@".value": @"a", @".priority": @1},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"c": @{@".value": @"c", @".priority": @3},
+ @"d": @{@".value": @"d", @".priority": @4}
+ }];
+
+ [node setValue:@{
+ @"d": @{@".value": @"d", @".priority": @0},
+ @"a": @{@".value": @"a", @".priority": @1},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"c": @{@".value": @"c", @".priority": @3}
+ }];
+ [self waitUntil:^BOOL{
+ return count == 1;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"d (null), "], @"Got move");
+
+ [moved setString:@""];
+
+ [node setValue:@{
+ @"d": @{@".value": @"d", @".priority": @0},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"c": @{@".value": @"c", @".priority": @3},
+ @"a": @{@".value": @"a", @".priority": @4}
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 2;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"a c, "], @"Got move");
+
+ [moved setString:@""];
+
+ [node setValue:@{
+ @"d": @{@".value": @"d", @".priority": @0},
+ @"c": @{@".value": @"c", @".priority": @0.5},
+ @"b": @{@".value": @"b", @".priority": @2},
+ @"a": @{@".value": @"a", @".priority": @4}
+ }];
+
+ [self waitUntil:^BOOL{
+ return count == 3;
+ }];
+
+ XCTAssertTrue([moved isEqualToString:@"c d, "], @"Got move");
+}
+
+- (void) testCase595NoChildMovedEventWhenDeletingPrioritizedGrandchild {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block int moves = 0;
+ [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snapshot) {
+ moves++;
+ }];
+
+ __block BOOL ready = NO;
+ [[node child:@"test/foo"] setValue:@42 andPriority:@"5"];
+ [[node child:@"test/foo2"] setValue:@42 andPriority:@"10"];
+ [[node child:@"test/foo"] removeValue];
+ [[node child:@"test/foo"] removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ XCTAssertTrue(moves == 0, @"Nothing should have moved");
+
+}
+
+- (void) testCanSetAValueWithPriZero {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ snap = s;
+ }];
+
+ [node setValue:@"test" andPriority:@0];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertEqualObjects([snap value], @"test", @"Proper value");
+ XCTAssertEqualObjects([snap priority], @0, @"Proper value");
+}
+
+- (void) testCanSetObjectWithPriZero {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) {
+ snap = s;
+ }];
+
+ [node setValue:@{@"x": @"test", @"y": @7} andPriority:@0];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertEqualObjects([[snap value] objectForKey:@"x"], @"test", @"Proper value");
+ XCTAssertEqualObjects([[snap value] objectForKey:@"y"], @7, @"Proper value");
+ XCTAssertEqualObjects([snap priority], @0, @"Proper value");
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FOrderByTests.h b/Example/Database/Tests/Integration/FOrderByTests.h
new file mode 100644
index 0000000..ce7b6f6
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrderByTests.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "FTestBase.h"
+
+
+@interface FOrderByTests : FTestBase
+@end
diff --git a/Example/Database/Tests/Integration/FOrderByTests.m b/Example/Database/Tests/Integration/FOrderByTests.m
new file mode 100644
index 0000000..aea6b47
--- /dev/null
+++ b/Example/Database/Tests/Integration/FOrderByTests.m
@@ -0,0 +1,671 @@
+/*
+ * 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 "FOrderByTests.h"
+
+@interface FOrderByTests ()
+@end
+
+@implementation FOrderByTests
+
+
+- (void) testCanDefineAndUseAnIndex {
+ __block FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSArray *users = @[
+ @{@"name": @"Andrew", @"nuggets": @35},
+ @{@"name": @"Rob", @"nuggets": @40},
+ @{@"name": @"Greg", @"nuggets": @38}
+ ];
+
+ __block int setCount = 0;
+ [users enumerateObjectsUsingBlock:^(NSDictionary *user, NSUInteger idx, BOOL *stop) {
+ [[ref childByAutoId] setValue:user withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ setCount++;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return setCount == users.count;
+ }];
+
+ __block NSMutableArray *byNuggets = [[NSMutableArray alloc] init];
+ [[ref queryOrderedByChild:@"nuggets"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *user = snapshot.value;
+ [byNuggets addObject:user[@"name"]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return byNuggets.count == users.count;
+ }];
+
+ NSArray *expected = @[@"Andrew", @"Greg", @"Rob"];
+ XCTAssertEqualObjects(byNuggets, expected, @"Correct by-nugget ordering.");
+}
+
+- (void) testCanDefineAndUseDeepIndex {
+ __block FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSArray *users = @[
+ @{@"name": @"Andrew", @"deep": @{@"nuggets": @35}},
+ @{@"name": @"Rob", @"deep": @{@"nuggets": @40}},
+ @{@"name": @"Greg", @"deep": @{@"nuggets": @38}}
+ ];
+
+ __block int setCount = 0;
+ [users enumerateObjectsUsingBlock:^(NSDictionary *user, NSUInteger idx, BOOL *stop) {
+ [[ref childByAutoId] setValue:user withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ setCount++;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return setCount == users.count;
+ }];
+
+ __block NSMutableArray *byNuggets = [[NSMutableArray alloc] init];
+ [[ref queryOrderedByChild:@"deep/nuggets"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *user = snapshot.value;
+ [byNuggets addObject:user[@"name"]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return byNuggets.count == users.count;
+ }];
+
+ NSArray *expected = @[@"Andrew", @"Greg", @"Rob"];
+ XCTAssertEqualObjects(byNuggets, expected, @"Correct by-nugget ordering.");
+}
+
+- (void) testCanUsaAFallbackThenDefineTheSpecifiedIndex {
+ FTupleFirebase *tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *reader = tuple.one, *writer = tuple.two;
+
+ NSDictionary *foo1 = @{
+ @"a" : @{@"order" : @2, @"foo" : @1},
+ @"b" : @{@"order" : @0},
+ @"c" : @{@"order" : @1, @"foo" : @NO},
+ @"d" : @{@"order" : @3, @"foo" : @"hello"}
+ };
+
+ NSDictionary *foo_e = @{@"order": @1.5, @"foo": @YES};
+ NSDictionary *foo_f = @{@"order": @4, @"foo": @{@"bar": @"baz"}};
+
+ [self waitForCompletionOf:writer setValue:foo1];
+
+ NSMutableArray *snaps = [[NSMutableArray alloc] init];
+ [[[reader queryOrderedByChild:@"order"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [snaps addObject:snapshot.value];
+ }];
+ WAIT_FOR(snaps.count == 1);
+
+ NSDictionary *expected = @{
+ @"d": @{@"order": @3, @"foo": @"hello"},
+ @"a": @{@"order": @2, @"foo": @1}
+ };
+ XCTAssertEqualObjects(snaps[0], expected, @"Got correct result");
+
+
+ [self waitForCompletionOf:[writer child:@"e"] setValue:foo_e];
+
+ [self waitForRoundTrip:reader];
+ NSLog(@"snaps: %@", snaps);
+ NSLog(@"snaps.count: %ld", (unsigned long) snaps.count);
+ XCTAssertEqual(snaps.count, (NSUInteger)1, @"Should still have one event.");
+
+ [self waitForCompletionOf:[writer child:@"f"] setValue:foo_f];
+
+ [self waitForRoundTrip:reader];
+ XCTAssertEqual(snaps.count, (NSUInteger)2, @"Should have gotten another event.");
+ expected = @{
+ @"f": foo_f,
+ @"d": @{@"order": @3, @"foo": @"hello"}
+ };
+ XCTAssertEqualObjects(snaps[1], expected, @"Correct event.");
+}
+
+- (void) testSnapshotsAreIteratedInOrder {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @{@"nuggets": @60},
+ @"rob": @{@"nuggets": @56},
+ @"vassili": @{@"nuggets": @55.5},
+ @"tony": @{@"nuggets": @52},
+ @"greg": @{@"nuggets": @52}
+ };
+
+ NSArray *expectedOrder = @[@"greg", @"tony", @"vassili", @"rob", @"alex"];
+ NSArray *expectedPrevNames = @[[NSNull null], @"greg", @"tony", @"vassili", @"rob"];
+
+ NSMutableArray *valueOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init];
+
+ FIRDatabaseQuery *orderedRef = [ref queryOrderedByChild:@"nuggets"];
+
+ [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [valueOrder addObject:child.key];
+ }
+ }];
+
+ [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [addedOrder addObject:snapshot.key];
+ [addedPrevNames addObject:prevName ? prevName : [NSNull null]];
+ }];
+
+ [ref setValue:initial];
+ WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count);
+
+ XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order.");
+ XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events.");
+ XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order.");
+}
+
+- (void) testSnapshotsAreIteratedInOrderForValueIndex {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @60,
+ @"rob": @56,
+ @"vassili": @55.5,
+ @"tony": @52,
+ @"greg": @52
+ };
+
+ NSArray *expectedOrder = @[@"greg", @"tony", @"vassili", @"rob", @"alex"];
+ NSArray *expectedPrevNames = @[[NSNull null], @"greg", @"tony", @"vassili", @"rob"];
+
+ NSMutableArray *valueOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init];
+
+ FIRDatabaseQuery *orderedRef = [ref queryOrderedByValue];
+
+ [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [valueOrder addObject:child.key];
+ }
+ }];
+
+ [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [addedOrder addObject:snapshot.key];
+ [addedPrevNames addObject:prevName ? prevName : [NSNull null]];
+ }];
+
+ [ref setValue:initial];
+ WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count);
+
+ XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order.");
+ XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events.");
+ XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order.");
+}
+
+- (void) testFiresChildMovedEvents {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @{@"nuggets": @60},
+ @"rob": @{@"nuggets": @56},
+ @"vassili": @{@"nuggets": @55.5},
+ @"tony": @{@"nuggets": @52},
+ @"greg": @{@"nuggets": @52}
+ };
+
+ FIRDatabaseQuery *orderedRef = [ref queryOrderedByChild:@"nuggets"];
+
+ __block BOOL moved = NO;
+ [orderedRef observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ moved = YES;
+ XCTAssertEqualObjects(snapshot.key, @"greg", @"");
+ XCTAssertEqualObjects(prevName, @"rob", @"");
+ XCTAssertEqualObjects(snapshot.value, @{@"nuggets" : @57}, @"");
+ }];
+
+ [ref setValue:initial];
+ [[ref child:@"greg/nuggets"] setValue:@57];
+ WAIT_FOR(moved);
+}
+
+- (void) testDefineMultipleIndexesAtALocation {
+ FTupleFirebase *tuple = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *reader = tuple.one, *writer = tuple.two;
+
+ NSDictionary *foo1 = @{
+ @"a" : @{@"order" : @2, @"foo" : @2},
+ @"b" : @{@"order" : @0},
+ @"c" : @{@"order" : @1, @"foo" : @NO},
+ @"d" : @{@"order" : @3, @"foo" : @"hello"}
+ };
+
+
+ [self waitForCompletionOf:writer setValue:foo1];
+
+ FIRDatabaseQuery *fooOrder = [reader queryOrderedByChild:@"foo"];
+ FIRDatabaseQuery *orderOrder = [reader queryOrderedByChild:@"order"];
+ NSMutableArray *fooSnaps = [[NSMutableArray alloc] init];
+ NSMutableArray *orderSnaps = [[NSMutableArray alloc] init];
+
+ [[[fooOrder queryStartingAtValue:nil] queryEndingAtValue:@1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [fooSnaps addObject:snapshot.value];
+ }];
+
+ [[orderOrder queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [orderSnaps addObject:snapshot.value];
+ }];
+
+ WAIT_FOR(fooSnaps.count == 1 && orderSnaps.count == 1);
+
+ NSDictionary *expected = @{
+ @"b": @{@"order": @0},
+ @"c": @{@"order": @1, @"foo": @NO}
+ };
+ XCTAssertEqualObjects(fooSnaps[0], expected, @"");
+
+ expected = @{
+ @"d": @{@"order": @3, @"foo": @"hello"},
+ @"a": @{@"order": @2, @"foo": @2},
+ };
+ XCTAssertEqualObjects(orderSnaps[0], expected, @"");
+
+ [[writer child:@"a"] setValue:@{
+ @"order": @-1, @"foo": @1
+ }];
+
+ WAIT_FOR(fooSnaps.count == 2 && orderSnaps.count == 2);
+
+ expected = @{
+ @"a": @{@"order": @-1, @"foo": @1 },
+ @"b": @{@"order": @0},
+ @"c": @{@"order": @1, @"foo": @NO}
+ };
+ XCTAssertEqualObjects(fooSnaps[1], expected, @"");
+
+ expected = @{
+ @"d": @{@"order": @3, @"foo": @"hello"},
+ @"c": @{@"order": @1, @"foo": @NO}
+ };
+ XCTAssertEqualObjects(orderSnaps[1], expected, @"");
+}
+
+- (void) testCallbackRemovalWorks {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ __block int reads = 0;
+ FIRDatabaseHandle fooHandle, bazHandle;
+ fooHandle = [[ref queryOrderedByChild:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ [[ref queryOrderedByChild:@"bar"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ bazHandle = [[ref queryOrderedByChild:@"baz"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ reads++;
+ }];
+
+ [self waitForCompletionOf:ref setValue:@1];
+
+ XCTAssertEqual(reads, 4, @"");
+
+ [ref removeObserverWithHandle:fooHandle];
+ [self waitForCompletionOf:ref setValue:@2];
+ XCTAssertEqual(reads, 7, @"");
+
+ // should be a no-op, resulting in 3 more reads.
+ [[ref queryOrderedByChild:@"foo"] removeObserverWithHandle:bazHandle];
+ [self waitForCompletionOf:ref setValue:@3];
+ XCTAssertEqual(reads, 10, @"");
+
+ [[ref queryOrderedByChild:@"bar"] removeAllObservers];
+ [self waitForCompletionOf:ref setValue:@4];
+ XCTAssertEqual(reads, 12, @"");
+
+ // Now, remove everything.
+ [ref removeAllObservers];
+ [self waitForCompletionOf:ref setValue:@5];
+ XCTAssertEqual(reads, 12, @"");
+}
+
+- (void) testChildAddedEventsAreInTheCorrectOrder {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"a": @{@"value": @5},
+ @"c": @{@"value": @3}
+ };
+
+ NSMutableArray *added = [[NSMutableArray alloc] init];
+ [[ref queryOrderedByChild:@"value"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [added addObject:snapshot.key];
+ }];
+ [ref setValue:initial];
+
+ WAIT_FOR(added.count == 2);
+ NSArray *expected = @[@"c", @"a"];
+ XCTAssertEqualObjects(added, expected, @"");
+
+ [ref updateChildValues:@{
+ @"b": @{@"value": @4},
+ @"d": @{@"value": @2}
+ }];
+
+ WAIT_FOR(added.count == 4);
+ expected = @[@"c", @"a", @"d", @"b"];
+ XCTAssertEqualObjects(added, expected, @"");
+}
+
+- (void) testCanUseKeyIndex {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *data = @{
+ @"a": @{ @".priority": @10, @".value": @"a" },
+ @"b": @{ @".priority": @5, @".value": @"b" },
+ @"c": @{ @".priority": @20, @".value": @"c" },
+ @"d": @{ @".priority": @7, @".value": @"d" },
+ @"e": @{ @".priority": @30, @".value": @"e" },
+ @"f": @{ @".priority": @8, @".value": @"f" }
+ };
+
+ [self waitForCompletionOf:ref setValue:data];
+
+ __block BOOL valueDone = NO;
+ [[[ref queryOrderedByKey] queryStartingAtValue:@"c"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSMutableArray *keys = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [keys addObject:child.key];
+ }
+ NSArray *expected = @[@"c", @"d", @"e", @"f"];
+ XCTAssertEqualObjects(keys, expected, @"");
+ valueDone = YES;
+ }];
+ WAIT_FOR(valueDone);
+
+ NSMutableArray *keys = [[NSMutableArray alloc] init];
+ [[[ref queryOrderedByKey] queryLimitedToLast:5] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [keys addObject:child.key];
+ }
+ }];
+
+ WAIT_FOR(keys.count == 5);
+ NSArray *expected = @[@"b", @"c", @"d", @"e", @"f"];
+ XCTAssertEqualObjects(keys, expected, @"");
+}
+
+- (void) testQueriesWorkOnLeafNodes {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ [self waitForCompletionOf:ref setValue:@"leaf-node"];
+
+ __block BOOL valueDone = NO;
+ [[[ref queryOrderedByChild:@"foo"] queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(snapshot.value, [NSNull null]);
+ valueDone = YES;
+ }];
+ WAIT_FOR(valueDone);
+}
+
+- (void) testUpdatesForUnindexedQuery {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *reader = refs.one;
+ FIRDatabaseReference *writer = refs.two;
+
+ __block BOOL done = NO;
+ NSDictionary *value = @{ @"one": @{ @"index": @1, @"value": @"one" },
+ @"two": @{ @"index": @2, @"value": @"two" },
+ @"three": @{ @"index": @3, @"value": @"three" } };
+ [writer setValue:value withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ done = NO;
+
+ NSMutableArray *snapshots = [NSMutableArray array];
+
+ [[[reader queryOrderedByChild:@"index"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshots addObject:snapshot.value];
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ NSDictionary *expected = @{ @"two": @{ @"index": @2, @"value": @"two" },
+ @"three": @{ @"index": @3, @"value": @"three" } };
+
+ XCTAssertEqual(snapshots.count, (NSUInteger)1);
+ XCTAssertEqualObjects(snapshots[0], expected);
+
+ done = NO;
+ [[writer child:@"one/index"] setValue:@4];
+
+ WAIT_FOR(done);
+
+ expected = @{ @"one": @{ @"index": @4, @"value": @"one" },
+ @"three": @{ @"index": @3, @"value": @"three" } };
+ XCTAssertEqual(snapshots.count, (NSUInteger)2);
+ XCTAssertEqualObjects(snapshots[1], expected);
+}
+
+- (void) testServerRespectsKeyIndex {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+ NSDictionary *initial = @{
+ @"a": @1,
+ @"b": @2,
+ @"c": @3
+ };
+
+ // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected
+ // and actual data don't match
+ FIRDatabaseQuery *query = [[[reader queryOrderedByKey] queryStartingAtValue:@"b"] queryLimitedToFirst:2];
+
+ NSArray *expectedChildren = @[@"b", @"c"];
+
+ [self waitForCompletionOf:writer setValue:initial];
+
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+
+ __block BOOL done = NO;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ XCTAssertEqualObjects(expectedChildren, children, @"Got correct children");
+}
+
+- (void) testServerRespectsValueIndex {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+ NSDictionary *initial = @{
+ @"a": @1,
+ @"c": @2,
+ @"b": @3
+ };
+
+ // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected
+ // and actual data don't match
+ FIRDatabaseQuery *query = [[[reader queryOrderedByValue] queryStartingAtValue:@2] queryLimitedToFirst:2];
+
+ NSArray *expectedChildren = @[@"c", @"b"];
+
+ [self waitForCompletionOf:writer setValue:initial];
+
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+
+ __block BOOL done = NO;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ XCTAssertEqualObjects(expectedChildren, children, @"Got correct children");
+}
+
+- (void) testDeepUpdatesWorkWithQueries {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+
+ NSDictionary *initial = @{@"a": @{@"data": @"foo",
+ @"idx": @YES},
+ @"b": @{@"data": @"bar",
+ @"idx": @YES},
+ @"c": @{@"data": @"baz",
+ @"idx": @NO}};
+ [self waitForCompletionOf:writer setValue:initial];
+
+ FIRDatabaseQuery *query = [[reader queryOrderedByChild:@"idx"] queryEqualToValue:@YES];
+
+ NSDictionary* expected = @{@"a": @{@"data": @"foo",
+ @"idx": @YES},
+ @"b": @{@"data": @"bar",
+ @"idx": @YES}};
+
+ [self waitForExportValueOf:query toBe:expected];
+
+ NSDictionary *update = @{@"a/idx": @NO,
+ @"b/data": @"blah",
+ @"c/idx": @YES};
+ [self waitForCompletionOf:writer updateChildValues:update];
+
+ expected = @{@"b": @{@"data": @"blah",
+ @"idx": @YES},
+ @"c": @{@"data": @"baz",
+ @"idx": @YES}};
+ [self waitForExportValueOf:query toBe:expected];
+}
+
+- (void) testServerRespectsDeepIndex {
+ FTupleFirebase *refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference *writer = refs.one;
+ FIRDatabaseReference *reader = refs.two;
+
+
+ NSDictionary *initial = @{
+ @"a": @{@"deep":@{@"index":@1}},
+ @"c": @{@"deep":@{@"index":@2}},
+ @"b": @{@"deep":@{@"index":@3}}
+ };
+
+ // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected
+ // and actual data don't match
+ FIRDatabaseQuery *query = [[[reader queryOrderedByChild:@"deep/index"] queryStartingAtValue:@2] queryLimitedToFirst:2];
+
+ NSArray *expectedChildren = @[@"c", @"b"];
+
+ [self waitForCompletionOf:writer setValue:initial];
+
+ NSMutableArray *children = [[NSMutableArray alloc] init];
+
+ __block BOOL done = NO;
+ [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [children addObject:child.key];
+ }
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ XCTAssertEqualObjects(expectedChildren, children, @"Got correct children");
+}
+
+- (void) testStartAtEndAtWorksWithValueIndex {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initial = @{
+ @"alex": @60,
+ @"rob": @56,
+ @"vassili": @55.5,
+ @"tony": @52,
+ @"greg": @52
+ };
+
+ NSArray *expectedOrder = @[@"tony", @"vassili", @"rob"];
+ NSArray *expectedPrevNames = @[[NSNull null], @"tony", @"vassili"];
+
+ NSMutableArray *valueOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedOrder = [[NSMutableArray alloc] init];
+ NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init];
+
+ FIRDatabaseQuery *orderedRef = [[[ref queryOrderedByValue] queryStartingAtValue:@52 childKey:@"tony"] queryEndingAtValue:@59];
+
+ [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ for (FIRDataSnapshot *child in snapshot.children) {
+ [valueOrder addObject:child.key];
+ }
+ }];
+
+ [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ [addedOrder addObject:snapshot.key];
+ [addedPrevNames addObject:prevName ? prevName : [NSNull null]];
+ }];
+
+ [ref setValue:initial];
+ WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count);
+
+ XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order.");
+ XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events.");
+ XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order.");
+}
+
+- (void) testRemovingDefaultListenerRemovesNonDefaultListenWithLoadsAllData {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+
+ NSDictionary *initialData = @{ @"key": @"value" };
+ [self waitForCompletionOf:ref setValue:initialData];
+
+ [[ref queryOrderedByKey] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ }];
+
+ // Should remove both listener and should remove the listen sent to the server
+ [ref removeAllObservers];
+
+ __block id result = nil;
+ // This used to crash because a listener for [ref queryOrderedByKey] existed already
+ [[ref queryOrderedByKey] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ result = snapshot.value;
+ }];
+
+ WAIT_FOR(result);
+ XCTAssertEqualObjects(result, initialData);
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FPersist.h b/Example/Database/Tests/Integration/FPersist.h
new file mode 100644
index 0000000..5bdfff5
--- /dev/null
+++ b/Example/Database/Tests/Integration/FPersist.h
@@ -0,0 +1,22 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FPersist : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FPersist.m b/Example/Database/Tests/Integration/FPersist.m
new file mode 100644
index 0000000..2326e08
--- /dev/null
+++ b/Example/Database/Tests/Integration/FPersist.m
@@ -0,0 +1,489 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import <Foundation/Foundation.h>
+#import "FPersist.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FRepo_Private.h"
+#import "FTestHelpers.h"
+#import "FDevice.h"
+#import "FIRDatabaseQuery_Private.h"
+
+@implementation FPersist
+
+- (void) setUp {
+ [super setUp];
+
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+
+ NSString *baseDir = [FPersist getFirebaseDir];
+ // HACK: We want to clean up old persistence files from previous test runs, but on OSX, baseDir is going to be something
+ // like /Users/michael/Documents/firebase, and we probably shouldn't blindly delete it, since somebody might have actual
+ // documents there. We should probably change the directory where we store persistence on OSX to .firebase or something
+ // to avoid colliding with real files, but for now, we'll leave it and just manually delete each of the /0, /1, /2, etc.
+ // directories that may exist from previous test runs. As of now (2014/09/07), these directories only go up to ~50, but
+ // if we add a ton more tests, we may need to increase the 100. But I'm guessing we'll rewrite persistence and move the
+ // persistence folder before then though.
+ for(int i = 0; i < 100; i++) {
+ // TODO: This hack is uneffective because the format now follows different rules. Persistence really needs a purge
+ // option
+ NSString *dir = [NSString stringWithFormat:@"%@/%d", baseDir, i];
+ if ([fileManager fileExistsAtPath:dir]) {
+ NSError *error;
+ [[NSFileManager defaultManager] removeItemAtPath:dir error:&error];
+ if (error) {
+ XCTFail(@"Failed to clear persisted data at %@: %@", dir, error);
+ }
+ }
+ }
+}
+
+- (void) testSetIsResentAfterRestart {
+ FIRDatabaseReference *readerRef = [FTestHelpers getRandomNode];
+ NSString *url = [readerRef description];
+ FDevice* device = [[FDevice alloc] initOfflineWithUrl:url];
+
+ // Monitor the data at this location.
+ __block FIRDataSnapshot *readSnapshot = nil;
+ [readerRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ readSnapshot = snapshot;
+ }];
+
+ // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase.
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello", @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri"} }];
+ [[ref child:@"a"] setValue:@"a-val"];
+ [[ref child:@"c"] setPriority:@"c-pri"];
+ [ref updateChildValues:@{ @"b": @"b-val"}];
+ }];
+
+ // restart and wait for "idle" (so all pending puts should have been sent).
+ [device restartOnline];
+ [device waitForIdleUsingWaiter:self];
+
+ // Pending sets should have gone through.
+ id expected = @{
+ @"a": @"a-val",
+ @"b": @"b-val",
+ @"c": @{ @".value": @"hello", @".priority": @"c-pri" },
+ @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri" }
+ };
+ [self waitForExportValueOf:readerRef toBe:expected];
+
+ // Set the value to something else (12).
+ [readerRef setValue:@12];
+
+ // "restart" the app again and make sure it doesn't set it to 42 again.
+ [device restartOnline];
+ [device waitForIdleUsingWaiter:self];
+
+ // Make sure data is still 12.
+ [self waitForRoundTrip:readerRef];
+ XCTAssertEqual(readSnapshot.value, @12, @"Read data should still be 12.");
+ [device dispose];
+}
+
+- (void) testSetIsReappliedAfterRestart {
+ FDevice* device = [[FDevice alloc] initOffline];
+
+ // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase.
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello" }];
+ [[ref child:@"a"] setValue:@"a-val"];
+ [[ref child:@"c"] setPriority:@"c-pri"];
+ [ref updateChildValues:@{ @"b": @"b-val"}];
+ }];
+
+ // restart the app offline and observe the data.
+ [device restartOffline];
+
+ // Pending sets should be visible
+ id expected = @{
+ @"a": @"a-val",
+ @"b": @"b-val",
+ @"c": @{ @".value": @"hello", @".priority": @"c-pri" }
+ };
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForExportValueOf:ref toBe:expected];
+ }];
+ [device dispose];
+}
+
+- (void) testServerDataCachedOffline1 {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ __block BOOL done = NO;
+ id data = @{@"a": @1, @"b": @2};
+ [writerRef setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ // Wait for the data to get it cached.
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:data];
+ }];
+
+ // Should still be there after restart, offline.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:data];
+ }];
+
+ // Children should be there too.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:[ref child:@"a"] toBe:@1];
+ }];
+ [device dispose];
+}
+
+- (void) testServerDataCompleteness1 {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ id data = @{@"child": @{@"a": @1, @"b": @2 }, @"other": @"blah"};
+ [self waitForCompletionOf:writerRef setValue:data];
+
+ // Wait for each child to get it cached (but not the parent).
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:[ref child:@"child/a"] toBe:@1];
+ [self waitForValueOf:[ref child:@"child/b"] toBe:@2];
+ [self waitForValueOf:[ref child:@"other"] toBe:@"blah"];
+ }];
+
+ // Restart, offline, should get child_added events, but not value.
+ [device restartOffline];
+ __block BOOL gotA, gotB;
+ [device do:^(FIRDatabaseReference *ref) {
+ FIRDatabaseReference *childRef = [ref child:@"child"];
+ [childRef observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot.key isEqualToString:@"a"]) {
+ XCTAssertEqualObjects(snapshot.value, @1, @"Got a");
+ gotA = YES;
+ } else if ([snapshot.key isEqualToString:@"b"]) {
+ XCTAssertEqualObjects(snapshot.value, @2, @"Got a");
+ gotB = YES;
+ } else {
+ XCTFail(@"Unexpected child event.");
+ }
+ }];
+
+ // Listen for value events (which we should *not* get).
+ [childRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTFail(@"Got a value event with incomplete data!");
+ }];
+
+ // Wait for another location just to make sure we wait long enough that we /would/ get a value event if it
+ // was coming.
+ [self waitForValueOf:[ref child:@"other"] toBe:@"blah"];
+ }];
+
+ XCTAssertTrue(gotA && gotB, @"Got a and b.");
+ [device dispose];
+}
+
+- (void) testServerDataCompleteness2 {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ id data = @{@"a": @1, @"b": @2};
+ [self waitForCompletionOf:writerRef setValue:data];
+
+ // Wait for the children individually.
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:[ref child:@"a"] toBe:@1];
+ [self waitForValueOf:[ref child:@"b"] toBe:@2];
+ }];
+
+ // Should still be there after restart, offline.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // No-op. Just triggering a listen at this location.
+ }];
+ [self waitForValueOf:[ref child:@"a"] toBe:@1];
+ [self waitForValueOf:[ref child:@"b"] toBe:@2];
+ }];
+ [device dispose];
+}
+
+- (void)testServerDataLimit {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+ [self waitForCompletionOf:writerRef setValue:@{@"a": @1, @"b": @2, @"c": @3}];
+
+ // Cache limit(2) of the data.
+ [device do:^(FIRDatabaseReference *ref) {
+ FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2];
+ [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }];
+ }];
+
+ // We should be able to get limit(2) data offline, but not the whole node.
+ [device restartOffline];
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTFail(@"Got value event for whole node!");
+ }];
+
+ FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2];
+ [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }];
+ }];
+ [device dispose];
+}
+
+- (void)testRemoveWhileOfflineAndRestart {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+
+ [[writerRef child:@"test"] setValue:@"test"];
+ [device do:^(FIRDatabaseReference *ref) {
+ // Cache this location.
+ __block id val = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ val = snapshot.value;
+ }];
+ [self waitUntil:^BOOL {
+ return [val isEqual:@{@"test": @"test"}];
+ }];
+ }];
+ [device restartOffline];
+
+ __block BOOL done = NO;
+ [writerRef removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+ WAIT_FOR(done);
+
+ [device goOnline];
+ [device waitForIdleUsingWaiter:self];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:[NSNull null]];
+ }];
+ [device dispose];
+}
+
+
+- (void)testDeltaSyncAfterRestart {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+
+ [writerRef setValue:@"test"];
+
+ [device do:^(FIRDatabaseReference *ref) {
+ // Cache this location.
+ __block id val = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ val = snapshot.value;
+ }];
+ [self waitUntil:^BOOL {
+ return [val isEqual:@"test"];
+ }];
+ XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
+ }];
+ [device restartOnline];
+
+ [device waitForIdleUsingWaiter:self];
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForValueOf:ref toBe:@"test"];
+ XCTAssertEqual(ref.repo.dataUpdateCount, 0L, @"Should have gotten no updates.");
+ }];
+ [device dispose];
+}
+
+- (void)testDeltaSyncWorksWithUnfilteredQuery {
+ FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode];
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ];
+
+ // List must be large enough to trigger delta sync.
+ NSMutableDictionary *longList = [[NSMutableDictionary alloc] init];
+ for(NSInteger i = 0; i < 50; i++) {
+ NSString *key = [[writerRef childByAutoId] key];
+ longList[key] = @{ @"order": @1, @"text": @"This is an awesome message!" };
+ }
+
+ [writerRef setValue:longList];
+
+ [device do:^(FIRDatabaseReference *ref) {
+ // Cache this location.
+ [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList];
+ XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update.");
+ }];
+ [device restartOffline];
+
+ // Add a new child while the device is offline.
+ FIRDatabaseReference *newChildRef = [writerRef childByAutoId];
+ NSDictionary *newChild = @{ @"order": @50, @"text": @"This is a new appended child!" };
+
+ [self waitForCompletionOf:newChildRef setValue:newChild];
+ longList[[newChildRef key]] = newChild;
+
+ [device goOnline];
+ [device do:^(FIRDatabaseReference *ref) {
+ // Wait for updated value with new child.
+ [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList];
+ XCTAssertEqual(ref.repo.rangeMergeUpdateCount, 1L, @"Should have gotten a range merge update.");
+ }];
+ [device dispose];
+}
+
+- (void) testPutsAreRestoredInOrder {
+ FDevice *device = [[FDevice alloc] initOffline];
+
+ // Store puts which should have a putId with 10 which is lexiographical small than 9
+ [device do:^(FIRDatabaseReference *ref) {
+ for (int i = 0; i < 11; i++) {
+ [ref setValue:[NSNumber numberWithInt:i]];
+ }
+ }];
+
+ // restart the app offline and observe the data.
+ [device restartOffline];
+
+ // Make sure that the write with putId 10 wins, not 9
+ id expected = @10;
+ [device do:^(FIRDatabaseReference *ref) {
+ [self waitForExportValueOf:ref toBe:expected];
+ }];
+ [device dispose];
+}
+
+- (void) testStoreSetsPerf1 {
+ if (!runPerfTests) return;
+ // Disable persistence in FDevice for comparison without persistence
+ FDevice *device = [[FDevice alloc] initOnline];
+
+ __block BOOL done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ NSDate *start = [NSDate date];
+ [self writeChildren:ref count:1000 size:100 waitForComplete:NO];
+
+ [self waitForQueue:ref];
+
+ NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ [device dispose];
+}
+
+- (void) testStoreListenPerf1 {
+ if (!runPerfTests) return;
+ // Disable persistence in FDevice for comparison without persistence
+
+ // Write 1000 x 100-byte children, to read back.
+ unsigned int count = 1000;
+ FIRDatabaseReference *writer = [FTestHelpers getRandomNode];
+ [self writeChildren:writer count:count size:100];
+
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]];
+
+ __block BOOL done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ NSDate *start = [NSDate date];
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // Wait to make sure we're done persisting everything.
+ [self waitForQueue:ref];
+ XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
+ NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
+ done = YES;
+ }];
+ }];
+
+ WAIT_FOR(done);
+ [device dispose];
+}
+
+- (void) testRestoreListenPerf1 {
+ if (!runPerfTests) return;
+
+ // NOTE: Since this is testing restoration of data from cache after restarting, it only works with persistence on.
+
+ // Write 1000 * 100-byte children, to read back.
+ unsigned int count = 1000;
+ FIRDatabaseReference *writer = [FTestHelpers getRandomNode];
+ [self writeChildren:writer count:count size:100];
+
+ FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]];
+
+ // Get the data cached.
+ __block BOOL done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
+ done = YES;
+ }];
+ }];
+ WAIT_FOR(done);
+
+ // Restart offline and see how long it takes to restore the data from cache.
+ [device restartOffline];
+ done = NO;
+ [device do:^(FIRDatabaseReference *ref) {
+ NSDate *start = [NSDate date];
+ [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ // Wait to make sure we're done persisting everything.
+ XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data.");
+ [self waitForQueue:ref];
+ NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]);
+ done = YES;
+ }];
+ }];
+
+ WAIT_FOR(done);
+ [device dispose];
+}
+
+- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size {
+ [self writeChildren:writer count:count size:size waitForComplete:YES];
+}
+
+- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size waitForComplete:(BOOL)waitForComplete {
+ __block BOOL done = NO;
+
+ NSString *data = [self randomStringOfLength:size];
+ for(int i = 0; i < count; i++) {
+ [[writer childByAutoId] setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ if (i == (count - 1)) {
+ done = YES;
+ }
+ }];
+ }
+ if (waitForComplete) {
+ WAIT_FOR(done);
+ }
+}
+
+NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+- (NSString*) randomStringOfLength:(unsigned int)len {
+ NSMutableString *randomString = [NSMutableString stringWithCapacity: len];
+
+ for (int i=0; i<len; i++) {
+ [randomString appendFormat: @"%C", [letters characterAtIndex: arc4random() % [letters length]]];
+ }
+ return randomString;
+}
+
++ (NSString *) getFirebaseDir {
+ NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
+ NSString *documentsDir = [dirPaths objectAtIndex:0];
+ NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"];
+
+ return firebaseDir;
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FRealtime.h b/Example/Database/Tests/Integration/FRealtime.h
new file mode 100644
index 0000000..903ef49
--- /dev/null
+++ b/Example/Database/Tests/Integration/FRealtime.h
@@ -0,0 +1,22 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FTestBase.h"
+
+@interface FRealtime : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FRealtime.m b/Example/Database/Tests/Integration/FRealtime.m
new file mode 100644
index 0000000..e554bfe
--- /dev/null
+++ b/Example/Database/Tests/Integration/FRealtime.m
@@ -0,0 +1,605 @@
+/*
+ * 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 "FRealtime.h"
+#import "FTupleFirebase.h"
+#import "FRepoManager.h"
+#import "FUtilities.h"
+#import "FParsedUrl.h"
+#import "FIRDatabaseConfig_Private.h"
+
+@implementation FRealtime
+
+- (void) testUrlParsing {
+ FParsedUrl* parsed = [FUtilities parseUrl:@"http://www.example.com:9000"];
+ XCTAssertTrue([[parsed.path description] isEqualToString:@"/"], @"Got correct path");
+ XCTAssertTrue([parsed.repoInfo.host isEqualToString:@"www.example.com:9000"], @"Got correct host");
+ XCTAssertTrue([parsed.repoInfo.internalHost isEqualToString:@"www.example.com:9000"], @"Got correct host");
+ XCTAssertFalse(parsed.repoInfo.secure, @"Should not be secure, there's a port");
+
+ parsed = [FUtilities parseUrl:@"http://www.firebaseio.com/foo/bar"];
+ XCTAssertTrue([[parsed.path description] isEqualToString:@"/foo/bar"], @"Got correct path");
+ XCTAssertTrue([parsed.repoInfo.host isEqualToString:@"www.firebaseio.com"], @"Got correct host");
+ XCTAssertTrue([parsed.repoInfo.internalHost isEqualToString:@"www.firebaseio.com"], @"Got correct host");
+ XCTAssertTrue(parsed.repoInfo.secure, @"Should be secure, there's no port");
+}
+
+- (void) testCachingRedirects {
+ NSString* host = @"host.example.com";
+ NSString* host2 = @"host2.example.com";
+ NSString* internalHost = @"internal.example.com";
+ NSString* internalHost2 = @"internal2.example.com";
+
+ // Set host on first repo info
+ FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:host];
+ XCTAssertTrue([repoInfo.host isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfo.internalHost isEqualToString:host], @"Got correct host");
+
+ // Set internal host on first repo info
+ repoInfo.internalHost = internalHost;
+ XCTAssertTrue([repoInfo.host isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfo.internalHost isEqualToString:internalHost], @"Got correct host");
+
+ // Set up a second unrelated repo info to make sure caching is keyspaced properly
+ FRepoInfo* repoInfo2 = [[FRepoInfo alloc] initWithHost:host2 isSecure:YES withNamespace:host2];
+ XCTAssertTrue([repoInfo2.host isEqualToString:host2], @"Got correct host");
+ XCTAssertTrue([repoInfo2.internalHost isEqualToString:host2], @"Got correct host");
+
+ repoInfo2.internalHost = internalHost2;
+ XCTAssertTrue([repoInfo2.internalHost isEqualToString:internalHost2], @"Got correct host");
+
+ // Setting host on this repo info should also set the right internal host
+ FRepoInfo* repoInfoCached = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:host];
+ XCTAssertTrue([repoInfoCached.host isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfoCached.internalHost isEqualToString:internalHost], @"Got correct host");
+
+ [repoInfo clearInternalHostCache];
+ [repoInfo2 clearInternalHostCache];
+ [repoInfoCached clearInternalHostCache];
+
+ XCTAssertTrue([repoInfo.internalHost isEqualToString:host], @"Got correct host");
+ XCTAssertTrue([repoInfo2.internalHost isEqualToString:host2], @"Got correct host");
+ XCTAssertTrue([repoInfoCached.internalHost isEqualToString:host], @"Got correct host");
+}
+
+- (void) testOnDisconnectSetWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block NSNumber* readValue = @0;
+ __block NSNumber* writeValue = @0;
+ [[reader child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSNumber *val = [snapshot value];
+ if (![val isEqual:[NSNull null]]) {
+ readValue = val;
+ }
+ }];
+
+ [[writer child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ if (val != [NSNull null]) {
+ writeValue = val;
+ }
+ }];
+
+ [writer child:@"hello"];
+
+ __block BOOL ready = NO;
+ [[writer child:@"disconnected"] onDisconnectSetValue:@1 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref){
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [writer child:@"s"];
+
+ ready = NO;
+ [[writer child:@"disconnected"] onDisconnectSetValue:@2 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref){
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return [@2 isEqualToNumber:readValue] && [@2 isEqualToNumber:writeValue];
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectSetWithPriorityWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block BOOL sawNewValue = NO;
+ __block BOOL writerSawNewValue = NO;
+ [[reader child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = snapshot.value;
+ id pri = snapshot.priority;
+ if (val != [NSNull null] && pri != [NSNull null]) {
+ sawNewValue = [(NSNumber *) val boolValue] && [pri isEqualToString:@"abcd"];
+ }
+ }];
+
+ [[writer child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ id val = [snapshot value];
+ id pri = snapshot.priority;
+ if (val != [NSNull null] && pri != [NSNull null]) {
+ writerSawNewValue = [(NSNumber *) val boolValue] && [pri isEqualToString:@"abcd"];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [[writer child:@"disconnected"] onDisconnectSetValue:@YES andPriority:@"abcd" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawNewValue && writerSawNewValue;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectRemoveWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block BOOL ready = NO;
+ [[writer child:@"foo"] setValue:@"bar" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block BOOL sawRemove = NO;
+ __block BOOL writerSawRemove = NO;
+ [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ sawRemove = [[NSNull null] isEqual:snapshot.value];
+ }];
+
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ writerSawRemove = [[NSNull null] isEqual:snapshot.value];
+ }];
+
+ ready = NO;
+ [[writer child:@"foo"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawRemove && writerSawRemove;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectUpdateWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ [self waitForCompletionOf:[writer child:@"foo"] setValue:@{@"bar": @"a", @"baz": @"b"}];
+
+ __block BOOL sawNewValue = NO;
+ __block BOOL writerSawNewValue = NO;
+ [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ sawNewValue = [@{@"bar" : @"a", @"baz" : @"c", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ writerSawNewValue = [@{@"bar" : @"a", @"baz" : @"c", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ __block BOOL ready = NO;
+ [[writer child:@"foo"] onDisconnectUpdateChildValues:@{@"baz": @"c", @"bat": @"d"} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawNewValue && writerSawNewValue;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForWriter {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block int calls = 0;
+ [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"foo" : @{@"bar" : @"a", @"bam" : @"c"}};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForReader {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * reader = [FTestHelpers getRandomNode];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] child:reader.key];
+
+ __block int calls = 0;
+ [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"foo" : @{@"bar" : @"a", @"bam" : @"c"}};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForWriterWithQuery {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block int calls = 0;
+ [[[writer child:@"foo"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"bar" : @"a", @"bam" : @"c"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectTriggersSingleLocalValueEventForReaderWithQuery {
+ FIRDatabaseReference * reader = [FTestHelpers getRandomNode];
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] child:reader.key];
+
+ __block int calls = 0;
+ [[[reader child:@"foo"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ calls++;
+ XCTAssertTrue([snapshot.key isEqualToString:@"foo"], @"Got the right snapshot");
+ if (calls == 2) {
+ // second call, verify the data
+ NSDictionary *val = [snapshot value];
+ NSDictionary *expected = @{@"bar" : @"a", @"bam" : @"c"};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one");
+ } else if (calls > 2) {
+ XCTFail(@"Extra calls");
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return calls == 1;
+ }];
+
+ __block BOOL done = NO;
+ FIRDatabaseReference * child = [writer child:@"foo"];
+ [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}];
+ [child onDisconnectUpdateChildValues:@{@"bam": @"c"}];
+ [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return calls == 2;
+ }];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+- (void) testOnDisconnectDeepMergeTriggersOnlyOneValueEventForReaderWithQuery {
+ FIRDatabaseReference * reader = [FTestHelpers getRandomNode];
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block BOOL done = NO;
+ NSDictionary* toSet = @{@"a": @1, @"b": @{@"c": @YES, @"d": @"scalar", @"e": @{@"f": @"hooray"}}};
+ [writer setValue:toSet];
+ [[writer child:@"a"] onDisconnectSetValue:@2];
+ [[writer child:@"b/d"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+
+ __block int count = 2;
+ [[reader queryLimitedToLast:3] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ count++;
+ if (count == 1) {
+ // Loaded the data, kill the writer connection
+ [FRepoManager interrupt:writerCfg];
+ } else if (count == 2) {
+ NSDictionary *expected = @{@"a" : @2, @"b" : @{@"c" : @YES, @"e" : @{@"f" : @"hooray"}}};
+ XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Should see complete new snapshot");
+ } else {
+ XCTFail(@"Too many calls");
+ }
+ }];
+
+ WAIT_FOR(count == 2);
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+
+- (void) testOnDisconnectCancelWorks {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"];
+
+ FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+ FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key];
+
+ __block BOOL ready = NO;
+ [[writer child:@"foo"] setValue:@{@"bar": @"a", @"baz": @"b"} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block BOOL sawNewValue = NO;
+ __block BOOL writerSawNewValue = NO;
+ [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ sawNewValue = [@{@"bar" : @"a", @"baz" : @"b", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ NSDictionary *val = [snapshot value];
+ if (val) {
+ writerSawNewValue = [@{@"bar" : @"a", @"baz" : @"b", @"bat" : @"d"} isEqualToDictionary:val];
+ }
+ }];
+
+ ready = NO;
+ [[writer child:@"foo"] onDisconnectUpdateChildValues:@{@"baz": @"c", @"bat": @"d"}];
+ [[writer child:@"foo/baz"] cancelDisconnectOperationsWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ return sawNewValue && writerSawNewValue;
+ }];
+
+ [FRepoManager interrupt:readerCfg];
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+ [FRepoManager disposeRepos:readerCfg];
+}
+
+- (void) testOnDisconnectWithServerValuesWithLocalEvents {
+ FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"];
+ FIRDatabaseReference * node = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId];
+
+ __block FIRDataSnapshot *snap = nil;
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ NSDictionary* data = @{
+ @"a": @1,
+ @"b": @{
+ @".value": [FIRServerValue timestamp],
+ @".priority": [FIRServerValue timestamp]
+ }
+ };
+
+ __block BOOL done = NO;
+ [node onDisconnectSetValue:data andPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [node onDisconnectUpdateChildValues:@{ @"a": [FIRServerValue timestamp], @"c": [FIRServerValue timestamp] } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [FRepoManager interrupt:writerCfg];
+
+ [self waitUntil:^BOOL{
+ if ([snap value] != [NSNull null]) {
+ NSDictionary* val = [snap value];
+ done = (val[@"a"] && val[@"b"] && val[@"c"]);
+ }
+ return done;
+ }];
+
+ NSDictionary* value = [snap value];
+ 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");
+ XCTAssertEqualObjects([snap priority], [value objectForKey:@"a"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snap priority], [value objectForKey:@"b"], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([snap priority], [[snap childSnapshotForPath:@"b"] priority], @"Should get back matching ServerValue.TIMESTAMP");
+ XCTAssertEqualObjects([NSNull null], [[snap childSnapshotForPath:@"d"] value], @"Should get null for cancelled child");
+
+ // cleanup
+ [FRepoManager disposeRepos:writerCfg];
+}
+
+@end
diff --git a/Example/Database/Tests/Integration/FTransactionTest.h b/Example/Database/Tests/Integration/FTransactionTest.h
new file mode 100644
index 0000000..6bb7d4d
--- /dev/null
+++ b/Example/Database/Tests/Integration/FTransactionTest.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FTransactionTest : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Integration/FTransactionTest.m b/Example/Database/Tests/Integration/FTransactionTest.m
new file mode 100644
index 0000000..b78615b
--- /dev/null
+++ b/Example/Database/Tests/Integration/FTransactionTest.m
@@ -0,0 +1,1382 @@
+/*
+ * 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 "FTransactionTest.h"
+#import "FTestHelpers.h"
+#import "FEventTester.h"
+#import "FTupleEventTypeString.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FIRDatabaseConfig_Private.h"
+
+
+// HACK used by testUnsentTransactionsAreNotCancelledOnDisconnect to return one bad token and then a nil token.
+@interface FIROneBadTokenProvider : NSObject <FAuthTokenProvider> {
+ BOOL firstFetch;
+}
+@end
+
+@implementation FIROneBadTokenProvider
+- (instancetype) init {
+ self = [super init];
+ if (self) {
+ firstFetch = YES;
+ }
+ return self;
+}
+
+- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback {
+ // Simulate delay
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), [FIRDatabaseQuery sharedQueue], ^{
+ if (firstFetch) {
+ firstFetch = NO;
+ callback(@"bad-token", nil);
+ } else {
+ callback(nil, nil);
+ }
+ });
+}
+
+- (void) listenForTokenChanges:(fbt_void_nsstring)listener {
+}
+
+@end
+@implementation FTransactionTest
+
+- (void) testNewValueIsImmediatelyVisible {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL runOnce = NO;
+ [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){
+ runOnce = YES;
+ [currentValue setValue:@42];
+ return [FIRTransactionResult successWithValue:currentValue];
+ }];
+
+ [self waitUntil:^BOOL{
+ return runOnce;
+ }];
+
+ __block BOOL ready = NO;
+ [[node child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (!ready) {
+ NSNumber *val = [snapshot value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Got value set in transaction");
+ ready = YES;
+ }
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testNonAbortedTransactionSetsCommittedToTrueInCallback {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){
+ [currentValue setValue:@42];
+ return [FIRTransactionResult successWithValue:currentValue];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not have aborted");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testAbortedTransactionSetsCommittedToFalseInCallback {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){
+ return [FIRTransactionResult abort];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should have aborted");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testBugTestSetDataReconnectDoTransactionThatAbortsOnceDataArrivesVerifyCorrectEvents {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * reader = refs.one;
+
+ __block BOOL dataWritten = NO;
+ [[reader child:@"foo"] setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ dataWritten = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return dataWritten;
+ }];
+
+ FIRDatabaseReference * writer = refs.two;
+ __block int eventsReceived = 0;
+ [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if (eventsReceived == 0) {
+ NSString *val = [snapshot value];
+ XCTAssertTrue([val isEqualToString:@"temp value"], @"Got initial transaction value");
+ } else if (eventsReceived == 1) {
+ NSNumber *val = [snapshot value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Got hidden original value");
+ } else {
+ XCTFail(@"Too many events");
+ }
+ eventsReceived++;
+ }];
+
+ [[writer child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id current = [currentData value];
+ if (current == [NSNull null]) {
+ [currentData setValue:@"temp value"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"This transaction should never commit");
+ XCTAssertTrue(error == nil, @"This transaction should not have an error");
+ }];
+
+ [self waitUntil:^BOOL{
+ return eventsReceived == 2;
+ }];
+
+}
+
+- (void) testUseTransactionToCreateANodeMakeSureExactlyOneEventIsReceived {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block int events = 0;
+ __block BOOL done = NO;
+
+ [[node child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ events++;
+ if (events > 1) {
+ XCTFail(@"Too many events");
+ }
+ }];
+
+ [[node child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done && events == 1;
+ }];
+}
+
+- (void) testUseTransactionToUpdateTwoExistingChildNodesMakeSureEventsAreOnlyRaisedForChangedNode {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * node1 = [refs.one child:@"foo"];
+ FIRDatabaseReference * node2 = [refs.two child:@"foo"];
+
+ __block BOOL ready = NO;
+ [[node1 child:@"a"] setValue:@42];
+ [[node1 child:@"b"] setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ FEventTester* et = [[FEventTester alloc] initFrom:self];
+ NSArray* expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"a"] withEvent:FIRDataEventTypeValue withString:nil],
+ [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"b"] withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+ [et wait];
+
+ expect = @[
+ [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"b"] withEvent:FIRDataEventTypeValue withString:nil]
+ ];
+
+ [et addLookingFor:expect];
+
+ ready = NO;
+ [node2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSDictionary* toSet = @{@"a": @42, @"b": @87};
+ [currentData setValue:toSet];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ [et wait];
+}
+
+- (void) testTransactionOnlyCalledOnceWhenInitializingAnEmptyNode {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block BOOL updateCalled = NO;
+ [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ XCTAssertTrue(val == [NSNull null], @"Should be no value here to start with");
+ if (updateCalled) {
+ XCTFail(@"Should not be called again");
+ }
+ updateCalled = YES;
+ [currentData setValue:@{@"a": @5, @"b": @6}];
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [self waitUntil:^BOOL{
+ return updateCalled;
+ }];
+}
+
+- (void) testSecondTransactionGetsRunImmediatelyOnPreviousOutputAndOnlyRunsOnce {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL firstRun = NO;
+ __block BOOL firstDone = NO;
+ __block BOOL secondRun = NO;
+ __block BOOL secondDone = NO;
+
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertFalse(firstRun, @"Should not be run twice");
+ firstRun = YES;
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not fail");
+ firstDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return firstRun;
+ }];
+
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertFalse(secondRun, @"Should only run once");
+ secondRun = YES;
+ NSNumber* val = [currentData value];
+ XCTAssertTrue([val isEqualToNumber:@42], @"Should see result of last transaction");
+ [currentData setValue:@84];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not fail");
+ secondDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return secondRun;
+ }];
+
+ __block FIRDataSnapshot * snap = nil;
+ [ref1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap value] isEqualToNumber:@84], @"Should get updated value");
+
+ [self waitUntil:^BOOL{
+ return firstDone && secondDone;
+ }];
+
+ snap = nil;
+ [ref2 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [self waitUntil:^BOOL{
+ return snap != nil;
+ }];
+
+ XCTAssertTrue([[snap value] isEqualToNumber:@84], @"Should get updated value");
+}
+
+// The js test, "Set() cancels pending transactions and re-runs affected transactions.", does not cleanly port to ios
+// due to everything being asynchronous. Rather than attempt to mitigate the various race conditions inherent in a port,
+// I'm adding tests to cover the specific behaviors wrapped up in that one test.
+
+- (void) testSetCancelsPendingTransaction {
+ FIRDatabaseReference * node = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * nodeSnap = nil;
+ __block FIRDataSnapshot * nodeFooSnap = nil;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nodeSnap = snapshot;
+ }];
+
+ [[node child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nodeFooSnap = snapshot;
+ }];
+
+ __block BOOL firstDone = NO;
+ __block BOOL secondDone = NO;
+ __block BOOL firstRun = NO;
+
+ [[node child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertFalse(firstRun, @"Should only run once");
+ firstRun = YES;
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should not fail");
+ firstDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return nodeFooSnap != nil;
+ }];
+
+ XCTAssertTrue([[nodeFooSnap value] isEqualToNumber:@42], @"Got first value");
+
+ [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@{@"foo": @84, @"bar": @1}];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"This should not ever be committed");
+ secondDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return nodeSnap != nil;
+ }];
+
+ [[node child:@"foo"] setValue:@0];
+}
+
+// It's difficult to force a transaction re-run on ios, since everything is async. There is also an outstanding case that prevents
+// this test from being before a connection is established (#1981)
+/*
+- (void) testSetRerunsAffectedTransactions {
+
+ Firebase* node = [FTestHelpers getRandomNode];
+
+ __block BOOL ready = NO;
+ [[node.parent child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ ready = [[snapshot value] boolValue];
+ }];
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ __block FIRDataSnapshot* nodeSnap = nil;
+
+ [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ nodeSnap = snapshot;
+ NSLog(@"SNAP value: %@", [snapshot value]);
+ }];
+
+ __block BOOL firstDone = NO;
+ __block BOOL secondDone = NO;
+ __block BOOL firstRun = NO;
+ __block int secondCount = 0;
+ __block BOOL setDone = NO;
+
+ [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ STAssertFalse(firstRun, @"Should only run once");
+ firstRun = YES;
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ STAssertTrue(committed, @"Should not fail");
+ firstDone = YES;
+ }];
+
+ [[node child:@"bar"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSLog(@"RUNNING TRANSACTION");
+ secondCount++;
+ id val = [currentData value];
+ if (secondCount == 1) {
+ STAssertTrue(val == [NSNull null], @"Should not have a value");
+ [currentData setValue:@"first"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else if (secondCount == 2) {
+ NSLog(@"val: %@", val);
+ STAssertTrue(val == [NSNull null], @"Should not have a value");
+ [currentData setValue:@"second"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ STFail(@"Called too many times");
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ STAssertTrue(committed, @"Should eventually be committed");
+ secondDone = YES;
+ }];
+
+ [[node child:@"foo"] setValue:@0 andCompletionBlock:^(NSError *error) {
+ setDone = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return setDone;
+ }];
+
+ NSDictionary* expected = @{@"bar": @"second", @"foo": @0};
+ STAssertTrue([[nodeSnap value] isEqualToDictionary:expected], @"Got last value");
+
+ STAssertTrue(secondCount == 2, @"Should have re-run second transaction");
+}*/
+
+- (void) testTransactionSetSetWorks {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ XCTAssertTrue(val == [NSNull null], @"Initial data should be null");
+ [currentData setValue:@"hi!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not be an error");
+ XCTAssertTrue(committed, @"Should commit");
+ done = YES;
+ }];
+
+ [ref setValue:@"foo"];
+ [ref setValue:@"bar"];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testPriorityIsNotPreservedWhenSettingData {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block FIRDataSnapshot * snap = nil;
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ snap = snapshot;
+ }];
+
+ [ref setValue:@"test" andPriority:@5];
+
+ __block BOOL ready = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"new value"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ id val = [snap value];
+ id pri = [snap priority];
+ XCTAssertTrue(pri == [NSNull null], @"Got priority");
+ XCTAssertTrue([val isEqualToString:@"new value"], @"Get new value");
+}
+
+// Skipping test with nested transactions. Everything is async on ios, so new transactions just get placed in a queue
+
+- (void) testResultSnapshotIsPassedToOnComplete {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL done = NO;
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val == [NSNull null]) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+ // do it again for the aborted case
+
+ done = NO;
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val == [NSNull null]) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should not commit");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ // do it again on a fresh connection, for the aborted case
+ done = NO;
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val == [NSNull null]) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should not commit");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testTransactionAbortsAfter25Retries {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref.repo setHijackHash:YES];
+
+ __block int tries = 0;
+ __block BOOL done = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertTrue(tries < 25, @"Should not be more than 25 tries");
+ tries++;
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error != nil, @"Should fail, too many retries");
+ XCTAssertFalse(committed, @"Should not commit");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ [ref.repo setHijackHash:NO];
+}
+
+- (void) testSetShouldCancelSentTransactionsThatComeBackAsDatastale {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL ready = NO;
+ [ref1 setValue:@5 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ XCTAssertTrue(val == [NSNull null], @"No current value");
+ [currentData setValue:@72];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error != nil, @"Should abort");
+ XCTAssertFalse(committed, @"Should not commit");
+ ready = YES;
+ }];
+
+ [ref2 setValue:@32];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testUpdateShouldNotCancelUnrelatedTransactions {
+ FIRDatabaseReference* ref = [FTestHelpers getRandomNode];
+
+ __block BOOL fooTransactionDone = NO;
+ __block BOOL barTransactionDone = NO;
+
+ [self waitForCompletionOf:[ref child:@"foo"] setValue:@"oldValue"];
+
+ [ref.repo setHijackHash:YES];
+
+ // This transaction should get cancelled as we update "foo" later on.
+ [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@72];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error != nil, @"Should abort");
+ XCTAssertFalse(committed, @"Should not commit");
+ fooTransactionDone = YES;
+ }];
+
+ // This transaction should not get cancelled since we don't update "bar".
+ [[ref child:@"bar"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@72];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ // Note: In rare cases, this might get aborted since failed transactions (forced by setHijackHash) are only
+ // retried 25 times. If we hit this limit before we stop hijacking the hash below, this test will flake.
+ XCTAssertTrue(error == nil, @"Should not abort");
+ XCTAssertTrue(committed, @"Should commit");
+ barTransactionDone = YES;
+ }];
+
+ NSDictionary *udpateData = @{ @"foo": @"newValue",
+ @"boo": @"newValue",
+ @"doo/foo": @"newValue",
+ @"loo" : @{ @"doo": @{ @"boo":@"newValue"}}} ;
+
+ [self waitForCompletionOf:ref updateChildValues:udpateData];
+ XCTAssertTrue(fooTransactionDone, "Should have gotten cancelled before the update");
+ XCTAssertFalse(barTransactionDone, "Should run after the update");
+ [ref.repo setHijackHash:NO];
+
+ WAIT_FOR(barTransactionDone);
+}
+
+- (void) testTransactionOnWackyUnicode {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL ready = NO;
+ [ref1 setValue:@"♜♞♝♛♚♝♞♜" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+
+ ready = NO;
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ XCTAssertTrue([val isEqualToString:@"♜♞♝♛♚♝♞♜"], @"Got crazy unicode");
+ }
+ [currentData setValue:@"♖♘♗♕♔♗♘♖"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not abort");
+ XCTAssertTrue(committed, @"Should commit");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testImmediatelyAbortedTransactions {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult abort];
+ }];
+
+ __block BOOL ready = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult abort];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"No error occurred, we just aborted");
+ XCTAssertFalse(committed, @"Should not commit");
+ ready = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return ready;
+ }];
+}
+
+- (void) testAddingToAnArrayWithATransaction {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+
+ __block BOOL done = NO;
+ [ref setValue:@[@"cat", @"horse"] withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ NSArray* arr = val;
+ NSMutableArray* toSet = [arr mutableCopy];
+ [toSet addObject:@"dog"];
+ [currentData setValue:toSet];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ [currentData setValue:@[@"dog"]];
+ return [FIRTransactionResult successWithValue:currentData];
+ }
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ NSArray* val = [snapshot value];
+ NSArray* expected = @[@"cat", @"horse", @"dog"];
+ XCTAssertTrue([val isEqualToArray:expected], @"Got whole array");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testMergedTransactionsHaveCorrectSnapshotInOnComplete {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * node1 = refs.one;
+ FIRDatabaseReference * node2 = refs.two;
+
+ __block BOOL done = NO;
+ [node1 setValue:@{@"a": @0} withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ __block BOOL transaction1Done = NO;
+ __block BOOL transaction2Done = NO;
+
+ [node2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ XCTAssertTrue([@{@"a": @0} isEqualToDictionary:val], @"Got initial data");
+ }
+ [currentData setValue:@{@"a": @1}];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ XCTAssertTrue([snapshot.key isEqualToString:node2.key], @"Correct snapshot name");
+ NSDictionary* val = [snapshot value];
+ // Per new behavior, will include the accepted value of the transaction, if it was successful.
+ NSDictionary* expected = @{@"a": @1};
+ XCTAssertTrue([val isEqualToDictionary:expected], @"Got final result");
+ transaction1Done = YES;
+ }];
+
+ [[node2 child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id val = [currentData value];
+ if (val != [NSNull null]) {
+ XCTAssertTrue([@1 isEqualToNumber:val], @"Got initial data");
+ }
+ [currentData setValue:@2];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(committed, @"Should commit");
+ XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Correct snapshot name");
+ NSNumber* val = [snapshot value];
+ NSNumber* expected = @2;
+ XCTAssertTrue([val isEqualToNumber:expected], @"Got final result");
+ transaction2Done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return transaction1Done && transaction2Done;
+ }];
+}
+
+// Skipping two tests on nested calls. Since iOS uses a work queue, nested calls don't actually happen synchronously, so they aren't problematic
+
+- (void) testPendingTransactionsAreCancelledOnDisconnect {
+ FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"pending-transactions"];
+ FIRDatabaseReference * ref = [[[FIRDatabaseReference alloc] initWithConfig:cfg] childByAutoId];
+
+ __block BOOL done = NO;
+ [[ref child:@"a"] setValue:@"initial" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ done = NO;
+ [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"new"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertFalse(committed, @"Should not commit");
+ XCTAssertTrue(error != nil, @"Should be an error");
+ done = YES;
+ }];
+
+ [FRepoManager interrupt:cfg];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ // cleanup
+ [FRepoManager interrupt:cfg];
+ [FRepoManager disposeRepos:cfg];
+}
+
+- (void) testTransactionWithoutLocalEvents1 {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ NSMutableArray* values = [[NSMutableArray alloc] init];
+ [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [values addObject:[snapshot value]];
+ }];
+
+ [self waitUntil:^BOOL{
+ // get initial data
+ return values.count > 0;
+ }];
+
+ __block BOOL done = NO;
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"hello!"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not be an error");
+ XCTAssertTrue(committed, @"Committed");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"got correct snapshot");
+ done = YES;
+ } withLocalEvents:NO];
+
+ NSArray* expected = @[[NSNull null]];
+ XCTAssertTrue([values isEqualToArray:expected], @"Should not have gotten any values yet");
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ expected = @[[NSNull null], @"hello!"];
+ XCTAssertTrue([values isEqualToArray:expected], @"Should have the new value now");
+}
+
+- (void) testTransactionWithoutLocalEvents2 {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+ int SETS = 4;
+
+ [ref1.repo setHijackHash:YES];
+
+ NSMutableArray* events = [[NSMutableArray alloc] init];
+ [ref1 setValue:@0];
+ [ref1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [events addObject:[snapshot value]];
+ }];
+
+ [self waitUntil:^BOOL{
+ return events.count > 0;
+ }];
+
+ NSArray* expected = @[@0];
+ XCTAssertTrue([events isEqualToArray:expected], @"Got initial set");
+
+ __block int retries = 0;
+ __block BOOL done = NO;
+ [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ retries++;
+ id val = [currentData value];
+ NSNumber* num = @0;
+ if (val != [NSNull null]) {
+ num = val;
+ }
+ int eventCount = [num intValue];
+ if (eventCount == SETS - 1) {
+ [ref1.repo setHijackHash:NO];
+ }
+
+ [currentData setValue:@"txn result"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertTrue(error == nil, @"Should not be an error");
+ XCTAssertTrue(committed, @"Committed");
+ XCTAssertTrue([[snapshot value] isEqualToString:@"txn result"], @"got correct snapshot");
+ done = YES;
+ } withLocalEvents:NO];
+
+ // Meanwhile, do sets from the second connection
+ for (int i = 0; i < SETS; ++i) {
+ __block BOOL setDone = NO;
+ [ref2 setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ setDone = YES;
+ }];
+ [self waitUntil:^BOOL{
+ return setDone;
+ }];
+ }
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+
+ XCTAssertTrue(retries > 0, @"Transaction should have retried");
+ XCTAssertEqualObjects([events lastObject], @"txn result", @"Final value matches expected value from txn");
+}
+
+// Skipping test of calling transaction from value callback. Since all api calls are async on iOS, nested calls are not a problem.
+
+- (void) testTransactionRevertsDataWhenAddADeeperListen {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * ref1 = refs.one;
+ FIRDatabaseReference * ref2 = refs.two;
+
+ __block BOOL done = NO;
+ [[ref1 child:@"y"] setValue:@"test" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) {
+ [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ if (currentData.value == [NSNull null]) {
+ [[currentData childDataByAppendingPath:@"x"] setValue:@5];
+ return [FIRTransactionResult successWithValue:currentData];
+ } else {
+ return [FIRTransactionResult abort];
+ }
+ }];
+
+ [[ref2 child:@"y"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ if ([snapshot.value isEqual:@"test"]) {
+ done = YES;
+ }
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testTransactionWithIntegerKeys {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+ NSDictionary* toSet = @{@"1": @1, @"5": @5, @"10": @10, @"20": @20};
+ [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@42];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"Error should be nil.");
+ XCTAssertTrue(committed, @"Transaction should have committed.");
+ done = YES;
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+// https://app.asana.com/0/5673976843758/9259161251948
+- (void) testBubbleAppTransactionBug {
+ FIRDatabaseReference * ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@1];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }];
+
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSNumber* val = currentData.value;
+ NSNumber *new = [NSNumber numberWithInt:(val.intValue + 42)];
+ [currentData setValue:new];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }];
+
+ [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@7];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ NSNumber* a = [currentData childDataByAppendingPath:@"a"].value;
+ NSNumber* b = [currentData childDataByAppendingPath:@"b"].value;
+ NSNumber *new = [NSNumber numberWithInt:a.intValue + b.intValue];
+ [currentData setValue:new];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"Error should be nil.");
+ XCTAssertTrue(committed, @"Committed should be true.");
+ XCTAssertEqualObjects(@50, snapshot.value, @"Result should be 50.");
+ done = YES;
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+// If we have cached data, transactions shouldn't run on null.
+- (void) testTransactionsAreRunInitiallyOnCurrentlyCachedData {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ id initialData = @{
+ @"a": @"a-val",
+ @"b": @"b-val"
+ };
+ __block BOOL done = NO;
+ __weak FIRDatabaseReference *weakRef = ref;
+ [ref setValue:initialData withCompletionBlock:^(NSError *error, FIRDatabaseReference *r) {
+ [weakRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ [weakRef runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertEqualObjects(currentData.value, initialData, @"Should be initial data.");
+ done = YES;
+ return [FIRTransactionResult abort];
+ }];
+ }];
+ }];
+
+ [self waitUntil:^BOOL{
+ return done;
+ }];
+}
+
+- (void) testMultipleLevels {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testLocalServerValuesEventuallyButNotImmediatelyMatchServerWithTxns {
+ 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 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:[FIRServerValue timestamp]];
+ [currentData setPriority:[FIRServerValue timestamp]];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {}];
+
+ [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] < 2000, @"Should have received a local event with a value close to timestamp");
+ XCTAssertTrue([now doubleValue] - [firstWriteSnap.priority doubleValue] < 2000, @"Should have received a local event with a priority close to timestamp");
+ XCTAssertTrue([now doubleValue] - [secondWriteSnap.value doubleValue] < 2000, @"Should have received a server event with a value close to timestamp");
+ XCTAssertTrue([now doubleValue] - [secondWriteSnap.priority doubleValue] < 2000, @"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) testTransactionWithQueryListen {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL done = NO;
+
+ [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ [[ref queryLimitedToFirst:1] observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) {
+ } withCancelBlock:^(NSError *error) {
+ }];
+
+ [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ XCTAssertEqualObjects([snapshot value], @1, @"Transaction value should match initial set");
+ done = YES;
+ }];
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testTransactionDoesNotPickUpCachedDataFromPreviousOnce {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * me = refs.one;
+ FIRDatabaseReference * other = refs.two;
+ __block BOOL done = NO;
+
+ [me setValue:@"not null" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [me observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [other setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id current = [currentData value];
+ if (current == [NSNull null]) {
+ [currentData setValue:@"it was null!"];
+ } else {
+ [currentData setValue:@"it was not null!"];
+ }
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ XCTAssertEqualObjects([snapshot value], @"it was null!", @"Transaction value should match remote null set");
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testTransactionDoesNotPickUpCachedDataFromPreviousTransaction {
+ FTupleFirebase* refs = [FTestHelpers getRandomNodePair];
+ FIRDatabaseReference * me = refs.one;
+ FIRDatabaseReference * other = refs.two;
+ __block BOOL done = NO;
+
+ [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"not null"];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [other setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+ done = NO;
+
+ [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id current = [currentData value];
+ if (current == [NSNull null]) {
+ [currentData setValue:@"it was null!"];
+ } else {
+ [currentData setValue:@"it was not null!"];
+ }
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"This transaction should not have an error");
+ XCTAssertTrue(committed, @"Should not have aborted");
+ XCTAssertEqualObjects([snapshot value], @"it was null!", @"Transaction value should match remote null set");
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void) testTransactionOnQueriedLocationDoesntRunInitiallyOnNull {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL txnDone = NO;
+
+ [self waitForCompletionOf:[ref childByAutoId] setValue:@{ @"a": @1, @"b": @2 }];
+
+ [[ref queryLimitedToFirst:1] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshot.ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ id expected = @{@"a" : @1, @"b" : @2};
+ XCTAssertEqualObjects(currentData.value, expected, @"");
+ [currentData setValue:[NSNull null]];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"");
+ XCTAssertTrue(committed, @"");
+ XCTAssertEqualObjects(snapshot.value, [NSNull null], @"");
+ txnDone = YES;
+ }];
+ }];
+
+ WAIT_FOR(txnDone);
+}
+
+- (void) testTransactionsRaiseCorrectChildChangedEventsOnQueries {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL txnDone = NO;
+ NSMutableArray *snapshots = [[NSMutableArray alloc] init];
+
+ [self waitForCompletionOf:ref setValue:@{ @"foo": @{ @"value": @1 }}];
+
+ FIRDatabaseQuery *query = [ref queryEndingAtValue:@(DBL_MIN)];
+
+ [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshots addObject:snapshot];
+ }];
+
+ [query observeEventType:FIRDataEventTypeChildChanged withBlock:^(FIRDataSnapshot *snapshot) {
+ [snapshots addObject:snapshot];
+ }];
+
+ [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [[currentData childDataByAppendingPath:@"value"] setValue:@2];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"");
+ XCTAssertTrue(committed, @"");
+ txnDone = YES;
+ } withLocalEvents:NO];
+
+ WAIT_FOR(txnDone);
+
+ XCTAssertTrue(snapshots.count == 2, @"");
+ FIRDataSnapshot *addedSnapshot = snapshots[0];
+ XCTAssertEqualObjects(addedSnapshot.key, @"foo", @"");
+ XCTAssertEqualObjects(addedSnapshot.value, @{ @"value": @1 }, @"");
+
+ FIRDataSnapshot *changedSnapshot = snapshots[1];
+ XCTAssertEqualObjects(changedSnapshot.key, @"foo", @"");
+ XCTAssertEqualObjects(changedSnapshot.value, @{ @"value": @2 }, @"");
+}
+
+- (void) testTransactionsUseLocalMerges {
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ __block BOOL txnDone = NO;
+ [ref updateChildValues:@{ @"foo": @"bar"}];
+
+ [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ XCTAssertEqualObjects(currentData.value, @"bar", @"Transaction value matches local updates");
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error, @"");
+ XCTAssertTrue(committed, @"");
+ txnDone = YES;
+ }];
+
+ WAIT_FOR(txnDone);
+}
+
+//See https://app.asana.com/0/15566422264127/23303789496881
+- (void)testOutOfOrderRemoveWritesAreHandledCorrectly
+{
+ FIRDatabaseReference *ref = [FTestHelpers getRandomNode];
+ [ref setValue:@{@"foo": @"bar"}];
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"transaction-1"];
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+ [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@"transaction-2"];
+ return [FIRTransactionResult successWithValue:currentData];
+ }];
+ __block BOOL done = NO;
+ // This will trigger an abort of the transaction which should not cause the client to crash
+ [ref updateChildValues:@{@"qux": @"quu"} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
+ XCTAssertNil(error);
+ done = YES;
+ }];
+
+ WAIT_FOR(done);
+}
+
+- (void)testUnsentTransactionsAreNotCancelledOnDisconnect {
+ // Hack: To trigger us to disconnect before restoring state, we inject a bad auth token.
+ // In real-world usage the much more common case is that we get redirected to a different
+ // server, but that's harder to manufacture from a test.
+ NSString *configName = @"testUnsentTransactionsAreNotCancelledOnDisconnect";
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:configName];
+ config.authTokenProvider = [[FIROneBadTokenProvider alloc] init];
+
+ // Queue a transaction offline.
+ FIRDatabaseReference *root = [[FIRDatabaseReference alloc] initWithConfig:config];
+ [root.database goOffline];
+ __block BOOL done = NO;
+ [[root childByAutoId] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) {
+ [currentData setValue:@0];
+ return [FIRTransactionResult successWithValue:currentData];
+ } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {
+ XCTAssertNil(error);
+ XCTAssertTrue(committed);
+ done = YES;
+ }];
+
+ [root.database goOnline];
+ WAIT_FOR(done);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m b/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m
new file mode 100644
index 0000000..cdc9e1c
--- /dev/null
+++ b/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m
@@ -0,0 +1,485 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+#import "FArraySortedDictionary.h"
+#import "FTreeSortedDictionary.h"
+
+@interface FArraySortedDictionaryTests : XCTestCase
+
+@end
+
+@implementation FArraySortedDictionaryTests
+
+- (NSComparator) defaultComparator {
+ return ^(id obj1, id obj2) {
+ if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) {
+ return [obj1 compare:obj2];
+ }
+ else {
+ if(obj1 < obj2) {
+ return (NSComparisonResult)NSOrderedAscending;
+ }
+ else if (obj1 > obj2) {
+ return (NSComparisonResult)NSOrderedDescending;
+ }
+ else {
+ return (NSComparisonResult)NSOrderedSame;
+ }
+ }
+ };
+}
+
+
+
+- (void)testCreateNode
+{
+ FImmutableSortedDictionary *map = [[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertEqual(map.count, 1, @"Contains one element");
+}
+
+- (void)testGetNilReturnsNil {
+ FImmutableSortedDictionary *map1 = [[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map1 get:nil]);
+
+ FImmutableSortedDictionary *map2 = [[[FArraySortedDictionary alloc] initWithComparator:^NSComparisonResult(id obj1, id obj2) {
+ return [obj1 compare:obj2];
+ }]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map2 get:nil]);
+}
+
+- (void)testSearchForSpecificKey {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+ XCTAssertNil([map get:@3], @"Properly not found object");
+}
+
+- (void)testRemoveKeyValuePair {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ FImmutableSortedDictionary* newMap = [map removeKey:@1];
+ XCTAssertEqualObjects([newMap get:@2], @2, @"Found second object");
+ XCTAssertNil([newMap get:@1], @"Properly not found object");
+
+ // Make sure the original one is not mutated
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+}
+
+- (void)testMoreRemovals {
+ FImmutableSortedDictionary *map = [[[[[[[[[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9]
+ insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2]
+ insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88];
+ XCTAssertNotNil([map get:@7], @"Found object");
+ XCTAssertNotNil([map get:@3], @"Found object");
+ XCTAssertNotNil([map get:@1], @"Found object");
+
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@7];
+ FImmutableSortedDictionary* m2 = [map removeKey:@3];
+ FImmutableSortedDictionary* m3 = [map removeKey:@1];
+
+ XCTAssertNil([m1 get:@7], @"Removed object");
+ XCTAssertNotNil([m1 get:@3], @"Found object");
+ XCTAssertNotNil([m1 get:@1], @"Found object");
+
+ XCTAssertNil([m2 get:@3], @"Removed object");
+ XCTAssertNotNil([m2 get:@7], @"Found object");
+ XCTAssertNotNil([m2 get:@1], @"Found object");
+
+
+ XCTAssertNil([m3 get:@1], @"Removed object");
+ XCTAssertNotNil([m3 get:@7], @"Found object");
+ XCTAssertNotNil([m3 get:@3], @"Found object");
+}
+
+- (void) testRemovalBug {
+ FImmutableSortedDictionary *map = [[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2]
+ insertKey:@3 withValue:@3];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found object");
+ XCTAssertEqualObjects([map get:@3], @3, @"Found object");
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@2];
+ XCTAssertEqualObjects([m1 get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([m1 get:@3], @3, @"Found object");
+ XCTAssertNil([m1 get:@2], @"Removed object");
+}
+
+- (void) testIncreasing {
+ int total = 20;
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map insertKey:item withValue:item];
+ }
+
+ XCTAssertTrue([map count] == 20, @"Check if all 100 objects are in the map");
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map removeKey:item];
+ }
+
+ XCTAssertTrue([map count] == 0, @"Check if all 100 objects were removed");
+}
+
+- (void) testOverride {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ insertKey:@10 withValue:@8];
+
+ XCTAssertEqualObjects([map get:@10], @8, @"Found first object");
+}
+- (void) testEmpty {
+ FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ removeKey:@10];
+
+ XCTAssertTrue([map isEmpty], @"Properly empty");
+
+}
+
+- (void) testEmptyGet {
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertNil([map get:@"something"], @"Properly nil");
+}
+
+- (void) testEmptyCount {
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([map count] == 0, @"Properly zero count");
+}
+
+- (void) testEmptyRemoval {
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([[map removeKey:@"sometjhing"] count] == 0, @"Properly zero count");
+}
+
+- (void) testReverseTraversal {
+ FImmutableSortedDictionary *map = [[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@5 withValue:@5]
+ insertKey:@3 withValue:@3]
+ insertKey:@2 withValue:@2]
+ insertKey:@4 withValue:@4];
+
+ __block int next = 5;
+ [map enumerateKeysAndObjectsReverse:YES usingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Properly equal");
+ next = next - 1;
+ }];
+}
+
+
+- (void) testInsertionAndRemovalOfAHundredItems {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+ NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ [toRemove addObject:[NSNumber numberWithInt:i]];
+ }
+
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, N, @"Check we traversed all of the items");
+
+ // remove them
+
+ for(int i = 0; i < N; i++) {
+ map = [map removeKey:[toRemove objectAtIndex:i]];
+ }
+
+
+ XCTAssertEqual([map count], 0, @"Check we removed all of the items");
+}
+
+- (void) shuffleArray:(NSMutableArray *)array {
+ NSUInteger count = [array count];
+ for(NSUInteger i = 0; i < count; i++) {
+ NSInteger nElements = count - i;
+ NSInteger n = (arc4random() % nElements) + i;
+ [array exchangeObjectAtIndex:i withObjectAtIndex:n];
+ }
+}
+
+- (void) testOrderIsCorrect {
+
+ NSArray* toInsert = [[NSArray alloc] initWithObjects:@1,@7,@8,@5,@2,@6,@4,@0,@3, nil];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < [toInsert count]; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == [toInsert count], @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block NSUInteger next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInteger:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInteger:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, [toInsert count], @"Check we traversed all of the items");
+}
+
+- (void) testPredecessorKey {
+ FImmutableSortedDictionary *map = [[[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9];
+
+ XCTAssertNil([map getPredecessorKey:@1], @"First object doesn't have a predecessor");
+ XCTAssertEqualObjects([map getPredecessorKey:@3], @1, @"@1");
+ XCTAssertEqualObjects([map getPredecessorKey:@4], @3, @"@3");
+ XCTAssertEqualObjects([map getPredecessorKey:@7], @4, @"@4");
+ XCTAssertEqualObjects([map getPredecessorKey:@9], @7, @"@7");
+ XCTAssertEqualObjects([map getPredecessorKey:@50], @9, @"@9");
+ XCTAssertThrows([map getPredecessorKey:@777], @"Expect exception about nonexistant key");
+}
+
+- (void) testEnumerator {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ NSEnumerator* enumerator = [map keyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = 0;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 1;
+ }
+}
+
+- (void) testReverseEnumerator {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ NSEnumerator* enumerator = [map reverseKeyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = N - 1;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue--;
+ }
+}
+
+- (void) testEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 12;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+}
+
+- (void) testReverseEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+}
+
+- (void)testConversionToTreeMap {
+ int N = SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD + 5;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *dict = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]];
+
+ for(int i = 0; i < N; i++) {
+ dict = [dict insertKey:toInsert[i] withValue:toInsert[i]];
+ if (i < SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) {
+ XCTAssertTrue([dict isKindOfClass:[FArraySortedDictionary class]],
+ @"We're below the threshold we should be an array backed implementation");
+ XCTAssertEqual(dict.count, i + 1, @"Size doesn't match");
+ } else {
+ XCTAssertTrue([dict isKindOfClass:[FTreeSortedDictionary class]],
+ @"We're above the threshold we should be a tree backed implementation");
+ XCTAssertEqual(dict.count, i + 1, @"Size doesn't match");
+ }
+ }
+
+ // check the order is correct
+ __block NSUInteger next = 0;
+ [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInteger:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInteger:next], @"Correct value");
+ next = next + 1;
+ }];
+}
+
+
+
+@end
+
diff --git a/Example/Database/Tests/Unit/FCompoundHashTest.m b/Example/Database/Tests/Unit/FCompoundHashTest.m
new file mode 100644
index 0000000..15e6d10
--- /dev/null
+++ b/Example/Database/Tests/Unit/FCompoundHashTest.m
@@ -0,0 +1,141 @@
+/*
+ * 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 "FCompoundHash.h"
+#import "FTestHelpers.h"
+#import "FEmptyNode.h"
+#import "FStringUtilities.h"
+#import "FEmptyNode.h"
+
+@interface FCompoundHashTest : XCTestCase
+
+@end
+
+@implementation FCompoundHashTest
+
+static FCompoundHashSplitStrategy NEVER_SPLIT_STRATEGY = ^BOOL(FCompoundHashBuilder *builder) {
+ return NO;
+};
+
+- (FCompoundHashSplitStrategy)splitAtPaths:(NSArray *)paths {
+ return ^BOOL(FCompoundHashBuilder *builder) {
+ return [paths containsObject:builder.currentPath];
+ };
+}
+
+- (void)testEmptyNodeYieldsEmptyHash {
+ FCompoundHash *hash = [FCompoundHash fromNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(hash.posts, @[]);
+ XCTAssertEqualObjects(hash.hashes, @[@""]);
+}
+
+- (void)testCompoundHashIsAlwaysFollowedByEmptyHash {
+ id<FNode> node = NODE(@{@"foo": @"bar"});
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(string:\"bar\"))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"foo")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testCompoundHashCanSplitAtPriority {
+ id<FNode> node = NODE((@{@"foo": @{@"!beforePriority": @"before", @".priority": @"prio", @"afterPriority": @"after"}, @"qux": @"qux"}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"foo/.priority")]]];
+ NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"!beforePriority\":(string:\"before\"),\".priority\":(string:\"prio\")))"];
+ NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"afterPriority\":(string:\"after\")),\"qux\":(string:\"qux\"))"];
+ XCTAssertEqualObjects(hash.posts, (@[PATH(@"foo/.priority"), PATH(@"qux")]));
+ XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""]));
+}
+
+- (void)testHashesPriorityLeafNodes {
+ id<FNode> node = NODE((@{@"foo": @{@".value": @"bar", @".priority": @"baz"}}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(priority:string:\"baz\":string:\"bar\"))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"foo")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testHashingFollowsFirebaseKeySemantics {
+ id<FNode> node = NODE((@{@"1": @"one", @"2": @"two", @"10": @"ten"}));
+ // 10 is after 2 in Firebase key semantics, but would be before 2 in string semantics
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"2")]]];
+ NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"1\":(string:\"one\"),\"2\":(string:\"two\"))"];
+ NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"10\":(string:\"ten\"))"];
+ XCTAssertEqualObjects(hash.posts, (@[PATH(@"2"), PATH(@"10")]));
+ XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""]));
+}
+
+- (void)testHashingOnChildBoundariesWorks {
+ id<FNode> node = NODE((@{@"bar": @{@"deep": @"value"}, @"foo": @{@"other-deep": @"value"}}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"bar/deep")]]];
+ NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"bar\":(\"deep\":(string:\"value\")))"];
+ NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"other-deep\":(string:\"value\")))"];
+ XCTAssertEqualObjects(hash.posts, (@[PATH(@"bar/deep"), PATH(@"foo/other-deep")]));
+ XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""]));
+}
+
+- (void)testCommasAreSetForNestedChildren {
+ id<FNode> node = NODE((@{@"bar": @{@"deep": @"value"}, @"foo": @{@"other-deep": @"value"}}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"bar\":(\"deep\":(string:\"value\")),\"foo\":(\"other-deep\":(string:\"value\")))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"foo/other-deep")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testQuotedStringsAndKeys {
+ id<FNode> node = NODE((@{@"\"": @"\\", @"\"\\\"\\": @"\"\\\"\\"}));
+ FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY];
+ NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"\\\"\":(string:\"\\\\\"),\"\\\"\\\\\\\"\\\\\":(string:\"\\\"\\\\\\\"\\\\\"))"];
+
+ XCTAssertEqualObjects(hash.posts, @[PATH(@"\"\\\"\\")]);
+ XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""]));
+}
+
+- (void)testDefaultSplitHasSensibleAmountOfHashes {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ for (int i = 0; i < 500; i++) {
+ // roughly 15-20 bytes serialized per node, 10k total
+ dict[[NSString stringWithFormat:@"%d", i]] = @"value";
+ }
+ id<FNode> node10k = NODE(dict);
+
+ dict = [NSMutableDictionary dictionary];
+ for (int i = 0; i < 5000; i++) {
+ // roughly 15-20 bytes serialized per node, 100k total
+ dict[[NSString stringWithFormat:@"%d", i]] = @"value";
+ }
+ id<FNode> node100k = NODE(dict);
+
+ dict = [NSMutableDictionary dictionary];
+ for (int i = 0; i < 50000; i++) {
+ // roughly 15-20 bytes serialized per node, 1M total
+ dict[[NSString stringWithFormat:@"%d", i]] = @"value";
+ }
+ id<FNode> node1M = NODE(dict);
+
+ FCompoundHash *hash10k = [FCompoundHash fromNode:node10k];
+ FCompoundHash *hash100k = [FCompoundHash fromNode:node100k];
+ FCompoundHash *hash1M = [FCompoundHash fromNode:node1M];
+ XCTAssertEqualWithAccuracy(hash10k.hashes.count, 15, 3);
+ XCTAssertEqualWithAccuracy(hash100k.hashes.count, 50, 5);
+ XCTAssertEqualWithAccuracy(hash1M.hashes.count, 150, 10);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FCompoundWriteTest.m b/Example/Database/Tests/Unit/FCompoundWriteTest.m
new file mode 100644
index 0000000..1e0a85e
--- /dev/null
+++ b/Example/Database/Tests/Unit/FCompoundWriteTest.m
@@ -0,0 +1,526 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FNode.h"
+#import "FSnapshotUtilities.h"
+#import "FCompoundWrite.h"
+#import "FEmptyNode.h"
+#import "FLeafNode.h"
+#import "FNamedNode.h"
+
+@interface FCompoundWriteTest : XCTestCase
+
+@end
+
+@implementation FCompoundWriteTest
+
+- (id<FNode>) leafNode {
+ static id<FNode> node = nil;
+ if (!node) {
+ node = [FSnapshotUtilities nodeFrom:@"leaf-node"];
+ }
+ return node;
+}
+
+- (id<FNode>) priorityNode {
+ static id<FNode> node = nil;
+ if (!node) {
+ node = [FSnapshotUtilities nodeFrom:@"prio"];
+ }
+ return node;
+}
+
+- (id<FNode>) baseNode {
+ static id<FNode> node = nil;
+ if (!node) {
+ NSDictionary *base = @{@"child-1" : @"value-1", @"child-2" : @"value-2"};
+ node = [FSnapshotUtilities nodeFrom:base];
+ }
+ return node;
+}
+
+- (void) assertAppliedCompoundWrite:(FCompoundWrite *)compoundWrite equalsNode:(id<FNode>)node withPriority:(id<FNode>)priority {
+ id<FNode> updatedNode = [compoundWrite applyToNode:node];
+ if (node.isEmpty) {
+ XCTAssertEqualObjects([FEmptyNode emptyNode], updatedNode,
+ @"Applied compound write should be empty. %@", updatedNode);
+ } else {
+ XCTAssertEqualObjects([node updatePriority:priority], updatedNode,
+ @"Applied compound write should equal node with priority. %@", updatedNode);
+ }
+}
+
+- (void) testEmptyMergeIsEmpty {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ XCTAssertTrue(compoundWrite.isEmpty, @"Empty write should be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithPriorityUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.priorityNode atKey:@".priority"];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Priority update should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.leafNode
+ atPath:[[FPath alloc] initWith:@"foo/bar"]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Update should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithRootUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.leafNode
+ atPath:[FPath empty]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Update at root should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithEmptyRootUpdateIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:[FEmptyNode emptyNode]
+ atPath:[FPath empty]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Empty root update should not be empty %@", compoundWrite);
+}
+
+- (void) testCompoundWriteWithRootPriorityUpdateAndChildMergeIsNotEmpty {
+ FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.priorityNode atKey:@".priority"];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@".priority"]];
+ XCTAssertFalse(compoundWrite.isEmpty, @"Compound write with root priority update and child merge should not be empty.");
+}
+
+- (void) testAppliesLeafOverwrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.leafNode, @"Should get leaf node once applied %@", updatedNode);
+}
+
+- (void) testAppliesChildrenOverwrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> childNode = [[FEmptyNode emptyNode] updateImmediateChild:@"child" withNewChild:self.leafNode];
+ compoundWrite = [compoundWrite addWrite:childNode atPath:[FPath empty]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, childNode, @"Child overwrite should work");
+}
+
+- (void) testAddsChildNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> expectedNode = [[FEmptyNode emptyNode] updateImmediateChild:@"child" withNewChild:self.leafNode];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atKey:@"child"];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding child node should work %@", updatedNode);
+}
+
+- (void) testAddsDeepChildNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"deep/deep/node"];
+ id<FNode> expectedNode = [[FEmptyNode emptyNode] updateChild:path withNewChild:self.leafNode];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Should add deep child node correctly");
+}
+
+- (void) testOverwritesExistingChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-1"];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:[path getFront] withNewChild:self.leafNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Overwriting existing child should work.");
+}
+
+- (void) testUpdatesExistingChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-1/foo"];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateChild:path withNewChild:self.leafNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Updating existing child should work");
+}
+
+- (void) testDoesntUpdatePriorityOnEmptyNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atKey:@".priority"];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:[FEmptyNode emptyNode] withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testUpdatesPriorityOnNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atKey:@".priority"];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:self.priorityNode];
+}
+
+- (void) testUpdatesPriorityOfChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-1/.priority"];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateChild:path withNewChild:self.priorityNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Updating priority of child should work.");
+}
+
+- (void) testDoesntUpdatePriorityOfNonExistentChild {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ FPath *path = [[FPath alloc] initWith:@"child-3/.priority"];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:path];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, self.baseNode, @"Should not update priority of nonexistent child");
+}
+
+- (void) testDeepUpdateExistingUpdates {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ NSDictionary *expectedChild1 = @{@"foo":@"new-foo-value", @"bar":@"bar-value", @"baz":@"baz-value"};
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep update with existing updates should work.");
+}
+
+- (void) testShallowUpdateRemovesDeepUpdate {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1"]];
+ NSDictionary *expectedChild1 = @{@"foo":@"foo-value", @"bar":@"bar-value"};
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Shallow update should remove deep udpates.");
+}
+
+- (void) testChildPriorityDoesntUpdateEmptyNodePriorityOnChildMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child-1/.priority"]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:[FEmptyNode emptyNode] withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testChildPriorityUpdatesPriorityOnChildMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child-1/.priority"]];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:self.priorityNode];
+}
+
+- (void) testChildPriorityUpdatesEmptyPriorityOnChildMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-1/.priority"]];
+ id<FNode> node = [[FLeafNode alloc] initWithValue:@"foo" withPriority:self.priorityNode];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testDeepPrioritySetWorksOnEmptyNodeWhenOtherSetIsAvailable {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"foo/.priority"]];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"foo/child"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ id<FNode> updatedPriority = [updatedNode getChild:[[FPath alloc] initWith:@"foo"]].getPriority;
+ XCTAssertEqualObjects(updatedPriority, self.priorityNode, @"Should get priority");
+}
+
+- (void) testChildMergeLooksIntoUpdateNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"foo"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:@"foo-value"];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Child merge should get updates.");
+}
+
+- (void) testChildMergeRemovesNodeOnDeeperPaths {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"foo/not/existing"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.leafNode];
+ id<FNode> expectedNode = [FEmptyNode emptyNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Should not have node.");
+}
+
+- (void) testChildMergeWithEmptyPathIsSameMerge {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ XCTAssertEqualObjects([compoundWrite childCompoundWriteAtPath:[FPath empty]], compoundWrite,
+ @"Child merge with empty path should be the same merge.");
+}
+
+- (void) testRootUpdateRemovesRootPriority {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@"foo"];
+ compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, update, @"Root update should remove root priority");
+}
+
+- (void) testDeepUpdateRemovesPriorityThere {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"foo/.priority"]];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@"bar"];
+ compoundWrite = [compoundWrite addWrite:update atPath:[[FPath alloc] initWith:@"foo"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:@{@"foo":@"bar"}];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep update should remove priority there");
+}
+
+- (void) testAddingUpdatesAtPathWorks {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
+ [updateDictionary setObject:@"foo-value" forKey:@"foo"];
+ [updateDictionary setObject:@"bar-value" forKey:@"bar"];
+ FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary];
+ compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[[FPath alloc] initWith:@"child-1"]];
+
+ NSDictionary *expectedChild1 = @{@"foo":@"foo-value", @"bar":@"bar-value"};
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding updates at a path should work.");
+}
+
+- (void) testAddingUpdatesAtRootWorks {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
+ [updateDictionary setObject:@"new-value-1" forKey:@"child-1"];
+ [updateDictionary setObject:[NSNull null] forKey:@"child-2"];
+ [updateDictionary setObject:@"value-3" forKey:@"child-3"];
+ FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary];
+ compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[FPath empty]];
+
+ NSDictionary *expected = @{@"child-1":@"new-value-1", @"child-3":@"value-3"};
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:expected];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding updates at root should work.");
+}
+
+- (void) testChildMergeOfRootPriorityWorks {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@".priority"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.priorityNode, @"Child merge of root priority should work.");
+}
+
+- (void) testCompleteChildrenOnlyReturnsCompleteOverwrites {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1"]];
+ NSArray *expectedChildren = @[[[FNamedNode alloc] initWithName:@"child-1" andNode:self.leafNode]];
+ NSArray *completeChildren = [compoundWrite completeChildren];
+ XCTAssertEqualObjects(completeChildren, expectedChildren, @"Complete children should only return on complete overwrites.");
+}
+
+- (void) testCompleteChildrenOnlyReturnsEmptyOverwrites {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-1"]];
+ NSArray *expectedChildren = @[[[FNamedNode alloc] initWithName:@"child-1" andNode:[FEmptyNode emptyNode]]];
+ NSArray *completeChildren = [compoundWrite completeChildren];
+ XCTAssertEqualObjects(completeChildren, expectedChildren, @"Complete children should return list with empty on empty overwrites.");
+}
+
+- (void) testCompleteChildrenDoesntReturnDeepOverwrites {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1/deep/path"]];
+ NSArray *expectedChildren = @[];
+ NSArray *completeChildren = [compoundWrite completeChildren];
+ XCTAssertEqualObjects(completeChildren, expectedChildren, @"Should not get complete children on deep overwrites.");
+}
+
+- (void) testCompleteChildrenReturnAllCompleteChildrenButNoIncomplete {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1/deep/path"]];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-2"]];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-3"]];
+ NSDictionary *expected = @{
+ @"child-2":self.leafNode,
+ @"child-3":[FEmptyNode emptyNode]
+ };
+ NSMutableDictionary *actual = [[NSMutableDictionary alloc] init];
+ for (FNamedNode *node in compoundWrite.completeChildren) {
+ [actual setObject:node.node forKey:node.name];
+ }
+ XCTAssertEqualObjects(actual, expected, @"Complete children should get returned, but not incomplete ones.");
+}
+
+- (void) testCompleteChildrenReturnAllChildrenForRootSet {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.baseNode atPath:[FPath empty]];
+
+ NSDictionary *expected = @{
+ @"child-1": [FSnapshotUtilities nodeFrom:@"value-1"],
+ @"child-2": [FSnapshotUtilities nodeFrom:@"value-2"]
+ };
+
+ NSMutableDictionary *actual = [[NSMutableDictionary alloc] init];
+ for (FNamedNode *node in compoundWrite.completeChildren) {
+ [actual setObject:node.node forKey:node.name];
+ }
+ XCTAssertEqualObjects(actual, expected, @"Complete children should return all children on root set.");
+}
+
+- (void) testEmptyMergeHasNoShadowingWrite {
+ XCTAssertFalse([[FCompoundWrite emptyWrite] hasCompleteWriteAtPath:[FPath empty]], @"Empty merge has no shadowing write.");
+}
+
+- (void) testCompoundWriteWithEmptyRootHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[FPath empty]];
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Empty write should have shadowing write at root.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"child"]], @"Empty write should have complete write at child.");
+}
+
+- (void) testCompoundWriteWithRootHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Root write should have shadowing write at root.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"child"]], @"Root write should have complete write at child.");
+}
+
+- (void) testCompoundWriteWithDeepUpdateHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"deep/update"]];
+ XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Deep write should not have complete write at root.");
+ XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"deep"]], @"Deep write should not have should have complete write at child.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"deep/update"]], @"Deep write should have complete write at deep child.");
+}
+
+- (void) testCompoundWriteWithPriorityUpdateHasShadowingWrite {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Write with priority at root should not have complete write at root.");
+ XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@".priority"]], @"Write with priority at root should have complete priority.");
+}
+
+- (void) testUpdatesCanBeRemoved {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ compoundWrite = [compoundWrite addWrite:update atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, self.baseNode, @"Updates should be removed.");
+}
+
+- (void) testDeepRemovesHasNoEffectOnOverlayingSet {
+ // TODO I don't get this one.
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ NSDictionary *expected = @{
+ @"foo":@"new-foo-value",
+ @"bar":@"bar-value",
+ @"baz":@"baz-value"
+ };
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expected]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep removes should have no effect on overlaying set.");
+}
+
+- (void) testRemoveAtPathWithoutSetIsWithoutEffect {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-2"]];
+ NSDictionary *expected = @{
+ @"foo":@"new-foo-value",
+ @"bar":@"bar-value",
+ @"baz":@"baz-value"
+ };
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expected]];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Removing at path without a set should have no effect.");
+}
+
+- (void) testCanRemovePriority {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@".priority"]];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:self.leafNode withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testRemovingOnlyAffectsRemovedPath {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ NSDictionary *updateDictionary = @{
+ @"child-1": @"new-value-1",
+ @"child-2": [NSNull null],
+ @"child-3": @"value-3"
+ };
+ FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary];
+ compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[FPath empty]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-2"]];
+
+ NSDictionary *expected = @{
+ @"child-1": @"new-value-1",
+ @"child-2": @"value-2",
+ @"child-3": @"value-3"
+ };
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:expected];
+ XCTAssertEqualObjects(updatedNode, expectedNode, @"Removing should only affected removed paths");
+}
+
+- (void) testRemoveRemovesAllDeeperSets {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"];
+ id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"];
+ compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]];
+ compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode];
+ XCTAssertEqualObjects(updatedNode, self.baseNode, @"Remove should remove deeper sets.");
+}
+
+- (void) testRemoveAtRootAlsoRemovesPriority {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:[[FLeafNode alloc] initWithValue:@"foo" withPriority:self.priorityNode] atPath:[FPath empty]];
+ compoundWrite = [compoundWrite removeWriteAtPath:[FPath empty]];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"];
+ [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:[FEmptyNode emptyNode]];
+}
+
+- (void) testUpdatingPriorityDoesntOverwriteLeafNode {
+ // TODO I don't get this one.
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child/.priority"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.leafNode, @"Updating priority should not overwrite leaf node.");
+}
+
+- (void) testUpdatingEmptyChildNodeDoesntOverwriteLeafNode {
+ FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
+ compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]];
+ compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child"]];
+ id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]];
+ XCTAssertEqualObjects(updatedNode, self.leafNode, @"Updating empty node should not overwrite leaf node.");
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRDataSnapshotTests.h b/Example/Database/Tests/Unit/FIRDataSnapshotTests.h
new file mode 100644
index 0000000..b69e7f2
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRDataSnapshotTests.h
@@ -0,0 +1,21 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+@interface FIRDataSnapshotTests : XCTestCase
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRDataSnapshotTests.m b/Example/Database/Tests/Unit/FIRDataSnapshotTests.m
new file mode 100644
index 0000000..2a442df
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRDataSnapshotTests.m
@@ -0,0 +1,449 @@
+/*
+ * 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 "FIRDataSnapshotTests.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FTestHelpers.h"
+#import "FLeafNode.h"
+#import "FChildrenNode.h"
+#import "FEmptyNode.h"
+#import "FImmutableSortedDictionary.h"
+#import "FUtilities.h"
+#import "FSnapshotUtilities.h"
+#import "FIRDatabaseReference.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FPathIndex.h"
+#import "FLeafNode.h"
+#import "FValueIndex.h"
+
+@implementation FIRDataSnapshotTests
+
+- (void)setUp
+{
+ [super setUp];
+
+ // Set-up code here.
+}
+
+- (void)tearDown
+{
+ // Tear-down code here.
+
+ [super tearDown];
+}
+
+- (FIRDataSnapshot *)snapshotFor:(id)jsonDict {
+ FIRDatabaseConfig *config = [FIRDatabaseConfig defaultConfig];
+ FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:@"example.com" isSecure:NO withNamespace:@"default"];
+ FIRDatabaseReference * dummyRef = [[FIRDatabaseReference alloc] initWithRepo:[FRepoManager getRepo:repoInfo config:config] path:[FPath empty]];
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:[FSnapshotUtilities nodeFrom:jsonDict]];
+ FIRDataSnapshot * snapshot = [[FIRDataSnapshot alloc] initWithRef:dummyRef indexedNode:indexed];
+ return snapshot;
+}
+
+- (void) testCreationLeafNodesVariousTypes {
+
+ id<FNode> fortyTwo = [FSnapshotUtilities nodeFrom:@42];
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@5 withPriority:fortyTwo];
+
+ XCTAssertEqualObjects(x.val, @5, @"Values are the same");
+ XCTAssertEqualObjects(x.getPriority, [FSnapshotUtilities nodeFrom:@42], @"Priority is the same");
+ XCTAssertTrue([x isLeafNode], @"Node is a leaf");
+
+ x = [[FLeafNode alloc] initWithValue:@"test"];
+ XCTAssertEqualObjects(x.value, @"test", @"Check if leaf node is holding onto a string value");
+
+ x = [[FLeafNode alloc] initWithValue:[NSNumber numberWithBool:YES]];
+ XCTAssertTrue([x.value boolValue], @"Check if leaf node is holding onto a YES boolean");
+
+ x = [[FLeafNode alloc] initWithValue:[NSNumber numberWithBool:NO]];
+ XCTAssertFalse([x.value boolValue], @"Check if leaf node is holding onto a NO boolean");
+}
+
+- (void) testUpdatingPriorityWithoutChangingOld {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test" withPriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]]];
+ FLeafNode* y = [x updatePriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:187]]];
+
+ // old node is the same
+ XCTAssertEqualObjects(x.value, @"test", @"Values of old node are the same");
+ XCTAssertEqualObjects(x.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]], @"Priority of old node is the same.");
+
+ // new node has the new priority but the old value
+ XCTAssertEqualObjects(y.value, @"test", @"Values of old node are the same");
+ XCTAssertEqualObjects(y.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:187]], @"Priority of new node is update");
+}
+
+- (void) testUpdateImmediateChildReturnsANewChildrenNode {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test" withPriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]]];
+ FChildrenNode* y = [x updateImmediateChild:@"test" withNewChild:[[FLeafNode alloc] initWithValue:@"foo"]];
+
+ XCTAssertFalse([y isLeafNode], @"New node is no longer a leaf");
+ XCTAssertEqualObjects(y.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]], @"Priority of new node is update");
+
+ XCTAssertEqualObjects([[y getImmediateChild:@"test"] val], @"foo", @"Child node has the correct value");
+}
+
+- (void) testGetImmediateChildOnLeafNode {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test"];
+ XCTAssertEqualObjects([x getImmediateChild:@"foo"], [FEmptyNode emptyNode], @"Get immediate child on leaf node returns empty node");
+}
+
+- (void) testGetChildReturnsEmptyNode {
+ FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test"];
+ XCTAssertEqualObjects([x getChild:[[FPath alloc] initWith:@"foo/bar"]], [FEmptyNode emptyNode], @"Get child returns an empty node.");
+}
+
+- (NSComparator) defaultComparator {
+ return ^(id obj1, id obj2) {
+ if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) {
+ return [obj1 compare:obj2];
+ }
+ else {
+ if(obj1 < obj2) {
+ return (NSComparisonResult)NSOrderedAscending;
+ }
+ else if (obj1 > obj2) {
+ return (NSComparisonResult)NSOrderedDescending;
+ }
+ else {
+ return (NSComparisonResult)NSOrderedSame;
+ }
+ }
+ };
+}
+
+- (void) testUpdateImmediateChildWithNewNode {
+ FImmutableSortedDictionary* children = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]];
+ FChildrenNode* x = [[FChildrenNode alloc] initWithChildren:children];
+ FLeafNode* newValue = [[FLeafNode alloc] initWithValue:@"new value"];
+ FChildrenNode* y = [x updateImmediateChild:@"test" withNewChild:newValue];
+
+ XCTAssertEqualObjects(x.children, children, @"Original object stays the same");
+ XCTAssertEqualObjects([y.children objectForKey:@"test"], newValue, @"New internal node with the proper new value");
+ XCTAssertEqualObjects([[y.children objectForKey:@"test"] val], @"new value", @"Check the payload");
+}
+
+- (void) testUpdatechildWithNewNode {
+ FImmutableSortedDictionary* children = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]];
+ FChildrenNode* x = [[FChildrenNode alloc] initWithChildren:children];
+ FLeafNode* newValue = [[FLeafNode alloc] initWithValue:@"new value"];
+ FChildrenNode* y = [x updateChild:[[FPath alloc] initWith:@"test/foo"] withNewChild:newValue];
+ XCTAssertEqualObjects(x.children, children, @"Original object stays the same");
+ XCTAssertEqualObjects([y getChild:[[FPath alloc] initWith:@"test/foo"]], newValue, @"Check if the updateChild held");
+ XCTAssertEqualObjects([[y getChild:[[FPath alloc] initWith:@"test/foo"]] val], @"new value", @"Check the payload");
+}
+
+- (void) testObjectTypes {
+ XCTAssertEqualObjects(@"string", [FUtilities getJavascriptType:@""], @"Check string type");
+ XCTAssertEqualObjects(@"string", [FUtilities getJavascriptType:@"moo"], @"Check string type");
+
+ XCTAssertEqualObjects(@"boolean", [FUtilities getJavascriptType:@YES], @"Check boolean type");
+ XCTAssertEqualObjects(@"boolean", [FUtilities getJavascriptType:@NO], @"Check boolean type");
+
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@5], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@5.5], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@0], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@8273482734], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@-2], @"Check number type");
+ XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@-2.11], @"Check number type");
+}
+
+- (void) testNodeHashWorksCorrectly {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{ @"intNode" : @4,
+ @"doubleNode" : @4.5623,
+ @"stringNode" : @"hey guys",
+ @"boolNode" : @YES }];
+
+ XCTAssertEqualObjects(@"eVih19a6ZDz3NL32uVBtg9KSgQY=", [[node getImmediateChild:@"intNode"] dataHash], @"Check integer node");
+ XCTAssertEqualObjects(@"vf1CL0tIRwXXunHcG/irRECk3lY=", [[node getImmediateChild:@"doubleNode"] dataHash], @"Check double node");
+ XCTAssertEqualObjects(@"CUNLXWpCVoJE6z7z1vE57lGaKAU=", [[node getImmediateChild:@"stringNode"] dataHash], @"Check string node");
+ XCTAssertEqualObjects(@"E5z61QM0lN/U2WsOnusszCTkR8M=", [[node getImmediateChild:@"boolNode"] dataHash], @"Check boolean node");
+ XCTAssertEqualObjects(@"6Mc4jFmNdrLVIlJJjz2/MakTK9I=", [node dataHash], @"Check compound node");
+}
+
+- (void) testNodeHashWorksCorrectlyWithPriorities {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{
+ @"root": @{ @"c": @{@".value": @99, @".priority": @"abc"}, @".priority" : @"def" }
+ }];
+
+ XCTAssertEqualObjects(@"Fm6tzN4CVEu5WxFDZUdTtqbTVaA=", [node dataHash], @"Check compound node");
+}
+
+- (void) testGetPredecessorChild {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"d": @YES, @"a": @YES, @"g": @YES, @"c": @YES, @"e": @YES}];
+
+ XCTAssertNil([node predecessorChildKey:@"a"],
+ @"Check the first one sorted properly");
+ XCTAssertEqualObjects([node predecessorChildKey:@"c"],
+ @"a", @"Check a comes before c");
+ XCTAssertEqualObjects([node predecessorChildKey:@"d"],
+ @"c", @"Check c comes before d");
+ XCTAssertEqualObjects([node predecessorChildKey:@"e"],
+ @"d", @"Check d comes before e");
+ XCTAssertEqualObjects([node predecessorChildKey:@"g"],
+ @"e", @"Check e comes before g");
+}
+
+- (void) testSortedChildrenGetPredecessorChildWorksCorrectly {
+ // XXX impl SortedChildren
+}
+
+- (void) testSortedChildrenUpdateImmediateChildrenWorksCorrectly {
+ // XXX imple SortedChildren
+}
+
+- (void) testDataSnapshotHasChildrenWorks {
+
+ FIRDataSnapshot * snap = [self snapshotFor:@{}];
+ XCTAssertFalse([snap hasChildren], @"Empty dict has no children");
+
+ snap = [self snapshotFor:@5];
+ XCTAssertFalse([snap hasChildren], @"Leaf node has no children");
+
+ snap = [self snapshotFor:@{@"x": @5}];
+ XCTAssertTrue([snap hasChildren], @"Properly has children");
+}
+
+- (void) testDataSnapshotValWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@5];
+ XCTAssertEqualObjects([snap value], @5, @"Leaf node values are correct");
+
+ snap = [self snapshotFor:@{}];
+ XCTAssertTrue([snap value] == [NSNull null], @"Snapshot value is properly null");
+
+ NSDictionary* dict = @{
+ @"x": @5,
+ @"y": @{
+ @"ya": @1,
+ @"yb": @2,
+ @"yc": @{ @"yca" : @3}
+ }
+ };
+
+ snap = [self snapshotFor:dict];
+ XCTAssertTrue([dict isEqualToDictionary:[snap value]], @"Check if the dictionaries are the same");
+}
+
+- (void) testDataSnapshotChildWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"x": @5, @"y": @{@"yy": @3, @"yz": @4}}];
+
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"x"] value], @5, @"Check x");
+ NSDictionary* dict = @{@"yy": @3, @"yz": @4};
+ XCTAssertTrue([[[snap childSnapshotForPath:@"y"] value] isEqualToDictionary:dict], @"Check y");
+
+ XCTAssertEqualObjects([[[snap childSnapshotForPath:@"y"] childSnapshotForPath:@"yy"] value], @3, @"Check y/yy");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"y/yz"] value], @4, @"Check y/yz");
+ XCTAssertTrue([[snap childSnapshotForPath:@"z"] value] == [NSNull null], @"Check nonexistent z");
+ XCTAssertTrue([[snap childSnapshotForPath:@"x/y"] value] == [NSNull null], @"Check value of existent internal node");
+ XCTAssertTrue([[[snap childSnapshotForPath:@"x"] childSnapshotForPath:@"y"] value] == [NSNull null], @"Check value of existent internal node");
+}
+
+- (void) testDataSnapshotHasChildWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"x": @5, @"y": @{@"yy": @3, @"yz": @4}}];
+
+ XCTAssertTrue([snap hasChild:@"x"], @"Has child");
+ XCTAssertTrue([snap hasChild:@"y/yy"], @"Has child");
+
+ XCTAssertFalse([snap hasChild:@"dinosaur dinosaucer"], @"No child");
+ XCTAssertFalse([[snap childSnapshotForPath:@"x"] hasChild:@"anything"], @"No child");
+ XCTAssertFalse([snap hasChild:@"x/anything/at/all"], @"No child");
+}
+
+- (void) testDataSnapshotNameWorks {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @{@"b": @{@"c": @5}}}];
+
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"a"] key], @"a", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"a/b/c"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"/a/b/c"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"/a/b/c/"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"////a///b////c///"] key], @"c", @"Check child key");
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"////"] key], [snap key], @"Check root key");
+
+ XCTAssertEqualObjects([[snap childSnapshotForPath:@"/z/q/r/v////m"] key], @"m", @"Should also work for nonexistent paths");
+}
+
+- (void) testDataSnapshotForEachWithNoPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @1, @"z": @26, @"m": @13, @"n": @14, @"c": @3, @"b": @2, @"e": @5}];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@:%@:", [child key], [child value] ];
+ }
+
+ XCTAssertTrue([out isEqualToString:@"a:1:b:2:c:3:e:5:m:13:n:14:z:26:"], @"Proper order");
+}
+
+- (void) testDataSnapshotForEachWorksWithNumericPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"a": @{@".value" : @1, @".priority": @26},
+ @"z": @{@".value" : @26, @".priority": @1},
+ @"m": @{@".value" : @13, @".priority": @14},
+ @"n": @{@".value" : @14, @".priority": @12},
+ @"c": @{@".value" : @3, @".priority": @24},
+ @"b": @{@".value" : @2, @".priority": @25},
+ @"e": @{@".value" : @5, @".priority": @22},
+ }];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@:%@:", [child key], [child value] ];
+ }
+
+ XCTAssertTrue([out isEqualToString:@"z:26:n:14:m:13:e:5:c:3:b:2:a:1:"], @"Proper order");
+}
+
+- (void) testDataSnapshotForEachWorksWithNumericPrioritiesAsStrings {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"a": @{@".value" : @1, @".priority": @"26"},
+ @"z": @{@".value" : @26, @".priority": @"1"},
+ @"m": @{@".value" : @13, @".priority": @"14"},
+ @"n": @{@".value" : @14, @".priority": @"12"},
+ @"c": @{@".value" : @3, @".priority": @"24"},
+ @"b": @{@".value" : @2, @".priority": @"25"},
+ @"e": @{@".value" : @5, @".priority": @"22"},
+ }];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@:%@:", [child key], [child value] ];
+ }
+
+ XCTAssertTrue([out isEqualToString:@"z:26:n:14:m:13:e:5:c:3:b:2:a:1:"], @"Proper order");
+}
+
+- (void) testDataSnapshotForEachWorksAlphaPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"a": @{@".value" : @1, @".priority": @"first"},
+ @"z": @{@".value" : @26, @".priority": @"second"},
+ @"m": @{@".value" : @13, @".priority": @"third"},
+ @"n": @{@".value" : @14, @".priority": @"fourth"},
+ @"c": @{@".value" : @3, @".priority": @"fifth"},
+ @"b": @{@".value" : @2, @".priority": @"sixth"},
+ @"e": @{@".value" : @5, @".priority": @"seventh"},
+ }];
+
+ NSMutableString* output = [[NSMutableString alloc] init];
+ NSMutableArray* priorities = [[NSMutableArray alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [output appendFormat:@"%@:%@:", child.key, child.value];
+ [priorities addObject:child.priority];
+ }
+
+ XCTAssertTrue([output isEqualToString:@"c:3:a:1:n:14:z:26:e:5:b:2:m:13:"], @"Proper order");
+ NSArray* expected = @[@"fifth", @"first", @"fourth", @"second", @"seventh", @"sixth", @"third"];
+ XCTAssertTrue([priorities isEqualToArray:expected], @"Correct priorities");
+ XCTAssertTrue(snap.childrenCount == 7, @"Got correct children count");
+}
+
+
+- (void) testDataSnapshotForEachWorksWithMixedPriorities {
+ FIRDataSnapshot * snap = [self snapshotFor:@{
+ @"alpha42": @{@".value": @1, @".priority": @"zed" },
+ @"noPriorityC": @{@".value": @1, @".priority": [NSNull null] },
+ @"alpha14": @{@".value": @1, @".priority": @"500" },
+ @"noPriorityB": @{@".value": @1, @".priority": [NSNull null] },
+ @"num80": @{@".value": @1, @".priority": @4000.1 },
+ @"alpha13": @{@".value": @1, @".priority": @"4000" },
+ @"alpha11": @{@".value": @1, @".priority": @"24" },
+ @"alpha41": @{@".value": @1, @".priority": @"zed" },
+ @"alpha20": @{@".value": @1, @".priority": @"horse" },
+ @"num20": @{@".value": @1, @".priority": @123 },
+ @"num70": @{@".value": @1, @".priority": @4000.01 },
+ @"noPriorityA": @{@".value": @1, @".priority": [NSNull null] },
+ @"alpha30": @{@".value": @1, @".priority": @"tree" },
+ @"alpha12": @{@".value": @1, @".priority": @"300" },
+ @"num60": @{@".value": @1, @".priority": @4000.001 },
+ @"alpha10": @{@".value": @1, @".priority": @"0horse" },
+ @"num42": @{@".value": @1, @".priority": @500 },
+ @"alpha40": @{@".value": @1, @".priority": @"zed" },
+ @"num40": @{@".value": @1, @".priority": @500 }
+ }];
+
+ NSMutableString* out = [[NSMutableString alloc] init];
+ for (FIRDataSnapshot * child in snap.children) {
+ [out appendFormat:@"%@, ", [child key]];
+ }
+
+ NSString* expected = @"noPriorityA, noPriorityB, noPriorityC, num20, num40, num42, num60, num70, num80, alpha10, alpha11, alpha12, alpha13, alpha14, alpha20, alpha30, alpha40, alpha41, alpha42, ";
+
+ XCTAssertTrue([expected isEqualToString:out], @"Proper ordering seen");
+
+}
+
+- (void) testIgnoresNullValues {
+ FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @1, @"b": [NSNull null]}];
+ XCTAssertFalse([snap hasChild:@"b"], @"Should not have b, it was null");
+}
+
+- (void)testNameComparator {
+ NSComparator keyComparator = [FUtilities keyComparator];
+ XCTAssertEqual(keyComparator(@"1234", @"1234"), NSOrderedSame, @"NameComparator compares ints");
+ XCTAssertEqual(keyComparator(@"1234", @"12345"), NSOrderedAscending, @"NameComparator compares ints");
+ XCTAssertEqual(keyComparator(@"4321", @"1234"), NSOrderedDescending, @"NameComparator compares ints");
+ XCTAssertEqual(keyComparator(@"1234", @"zzzz"), NSOrderedAscending, @"NameComparator priorities ints");
+ XCTAssertEqual(keyComparator(@"4321", @"12a"), NSOrderedAscending, @"NameComparator priorities ints");
+ XCTAssertEqual(keyComparator(@"abc", @"abcd"), NSOrderedAscending, @"NameComparator uses lexiographical sorting for strings.");
+ XCTAssertEqual(keyComparator(@"zzzz", @"aaaa"), NSOrderedDescending, @"NameComparator compares strings");
+ XCTAssertEqual(keyComparator(@"-1234", @"0"), NSOrderedAscending, @"NameComparator compares negative values");
+ XCTAssertEqual(keyComparator(@"-1234", @"-1234"), NSOrderedSame, @"NameComparator compares negative values");
+ XCTAssertEqual(keyComparator(@"-1234", @"-4321"), NSOrderedDescending, @"NameComparator compares negative values");
+ XCTAssertEqual(keyComparator(@"-1234", @"-"), NSOrderedAscending, @"NameComparator does not parse - as integer");
+ XCTAssertEqual(keyComparator(@"-", @"1234"), NSOrderedDescending, @"NameComparator does not parse - as integer");
+}
+
+- (void) testExistsWorks {
+ FIRDataSnapshot * snap;
+
+ snap = [self snapshotFor:@{}];
+ XCTAssertFalse([snap exists], @"Should not exist");
+
+ snap = [self snapshotFor:@{ @".priority": @"1" }];
+ XCTAssertFalse([snap exists], @"Should not exist");
+
+ snap = [self snapshotFor:[NSNull null]];
+ XCTAssertFalse([snap exists], @"Should not exist");
+
+ snap = [self snapshotFor:[NSNumber numberWithBool:YES]];
+ XCTAssertTrue([snap exists], @"Should exist");
+
+ snap = [self snapshotFor:@5];
+ XCTAssertTrue([snap exists], @"Should exist");
+
+ snap = [self snapshotFor:@{ @"x": @5 }];
+ XCTAssertTrue([snap exists], @"Should exist");
+}
+
+- (void) testUpdatingEmptyChildDoesntOverwriteLeafNode {
+ FLeafNode *node = [[FLeafNode alloc] initWithValue:@"value"];
+ XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@".priority"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@"child"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@"child/.priority"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@"child" withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@".priority" withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node.");
+}
+
+/* This was reported by a customer, which broke because 유주연 > 윤규완오빠 but also 윤규완오빠 > 유주연 with the default
+ * string comparison... */
+- (void)testUnicodeEquality {
+ FNamedNode *node1 = [[FNamedNode alloc] initWithName:@"a" andNode:[[FLeafNode alloc] initWithValue:@"유주연"]];
+ FNamedNode *node2 = [[FNamedNode alloc] initWithName:@"a" andNode:[[FLeafNode alloc] initWithValue:@"윤규완오빠"]];
+ id<FIndex> index = [FValueIndex valueIndex];
+
+ // x < y should imply y > x
+ XCTAssertEqual([index compareNamedNode:node1 toNamedNode:node2], -[index compareNamedNode:node2 toNamedNode:node1]);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRMutableDataTests.h b/Example/Database/Tests/Unit/FIRMutableDataTests.h
new file mode 100644
index 0000000..cd0cec7
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRMutableDataTests.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FIRMutableDataTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FIRMutableDataTests.m b/Example/Database/Tests/Unit/FIRMutableDataTests.m
new file mode 100644
index 0000000..d36f139
--- /dev/null
+++ b/Example/Database/Tests/Unit/FIRMutableDataTests.m
@@ -0,0 +1,113 @@
+/*
+ * 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 "FIRMutableDataTests.h"
+#import "FSnapshotUtilities.h"
+#import "FIRMutableData_Private.h"
+
+@implementation FIRMutableDataTests
+
+- (FIRMutableData *)dataFor:(id)input {
+
+ id<FNode> node = [FSnapshotUtilities nodeFrom:input];
+ return [[FIRMutableData alloc] initWithNode:node];
+}
+
+- (void) testDataForInWorksAlphaPriorities {
+ FIRMutableData * data = [self dataFor:@{
+ @"a": @{@".value" : @1, @".priority": @"first"},
+ @"z": @{@".value" : @26, @".priority": @"second"},
+ @"m": @{@".value" : @13, @".priority": @"third"},
+ @"n": @{@".value" : @14, @".priority": @"fourth"},
+ @"c": @{@".value" : @3, @".priority": @"fifth"},
+ @"b": @{@".value" : @2, @".priority": @"sixth"},
+ @"e": @{@".value" : @5, @".priority": @"seventh"},
+ }];
+
+ NSMutableString* output = [[NSMutableString alloc] init];
+ NSMutableArray* priorities = [[NSMutableArray alloc] init];
+ for (FIRMutableData * child in data.children) {
+ [output appendFormat:@"%@:%@:", child.key, child.value];
+ [priorities addObject:child.priority];
+ }
+
+ XCTAssertTrue([output isEqualToString:@"c:3:a:1:n:14:z:26:e:5:b:2:m:13:"], @"Proper order");
+ NSArray* expected = @[@"fifth", @"first", @"fourth", @"second", @"seventh", @"sixth", @"third"];
+ XCTAssertTrue([priorities isEqualToArray:expected], @"Correct priorities");
+ XCTAssertTrue(data.childrenCount == 7, @"Got correct children count");
+}
+
+- (void) testWritingMutableData {
+ FIRMutableData * data = [self dataFor:@{}];
+
+ data.value = @{@"a": @1, @"b": @2};
+ XCTAssertTrue([data hasChildren], @"Should have children node");
+ XCTAssertTrue(data.childrenCount == 2, @"Counts both children");
+ XCTAssertTrue([data hasChildAtPath:@"a"], @"Can see the children individually");
+
+ FIRMutableData * childData = [data childDataByAppendingPath:@"b"];
+ XCTAssertTrue([childData.value isEqualToNumber:@2], @"Get the correct child data");
+ childData.value = @3;
+
+ NSDictionary* expected = @{@"a": @1, @"b": @3};
+ XCTAssertTrue([data.value isEqualToDictionary:expected], @"Updates the parent");
+
+ int count = 0;
+ for (FIRDataSnapshot * __unused child in data.children) {
+ count++;
+ if (count == 1) {
+ [data childDataByAppendingPath:@"c"].value = @4;
+ }
+ }
+ XCTAssertTrue(count == 2, @"Should not iterate nodes added while iterating");
+ XCTAssertTrue(data.childrenCount == 3, @"Got the new node we added while iterating");
+ XCTAssertTrue([[data childDataByAppendingPath:@"c"].value isEqualToNumber:@4], @"Can see the value of the new node");
+}
+
+- (void) testMutableDataNavigation {
+ FIRMutableData * data = [self dataFor:@{@"a": @1, @"b": @2}];
+
+ XCTAssertNil(data.key, @"Root data has no key");
+
+ // Can get a child
+ FIRMutableData * childData = [data childDataByAppendingPath:@"b"];
+ XCTAssertTrue([childData.key isEqualToString:@"b"], @"Child has correct key");
+
+ // Can get a non-existent child
+ childData = [data childDataByAppendingPath:@"c"];
+ XCTAssertTrue(childData != nil, @"Wrapper should not be nil");
+ XCTAssertTrue([childData.key isEqualToString:@"c"], @"Child should have correct key");
+ XCTAssertTrue(childData.value == [NSNull null], @"Non-existent data has no value");
+ childData.value = @{@"d": @4};
+
+ NSDictionary* expected = @{@"a": @1, @"b": @2, @"c": @{@"d": @4}};
+ XCTAssertTrue([data.value isEqualToDictionary:expected], @"Setting non-existent child updates parent");
+}
+
+- (void) testPriorities {
+ FIRMutableData * data = [self dataFor:@{@"a": @1, @"b": @2}];
+
+ XCTAssertTrue(data.priority == [NSNull null], @"Should not be a priority");
+ data.priority = @"foo";
+ XCTAssertTrue([data.priority isEqualToString:@"foo"], @"Should now have a priority");
+ data.value = @3;
+ XCTAssertTrue(data.priority == [NSNull null], @"Setting a value overrides a priority");
+ data.priority = @4;
+ data.value = nil;
+ XCTAssertTrue(data.priority == [NSNull null], @"Removing the value does remove the priority");
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m
new file mode 100644
index 0000000..658a894
--- /dev/null
+++ b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m
@@ -0,0 +1,583 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+#import "FLevelDBStorageEngine.h"
+#import "FSnapshotUtilities.h"
+#import "FQueryParams.h"
+#import "FPathIndex.h"
+#import "FTrackedQuery.h"
+#import "FWriteRecord.h"
+#import "FTestHelpers.h"
+#import "FEmptyNode.h"
+
+@interface FLevelDBStorageEngineTests : XCTestCase
+
+@end
+
+@implementation FLevelDBStorageEngineTests
+
+- (FLevelDBStorageEngine *)cleanStorageEngine {
+ NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test-db"];
+ FLevelDBStorageEngine *db = [[FLevelDBStorageEngine alloc] initWithPath:path];
+ [db purgeEverything];
+ return db;
+}
+
+#define SAMPLE_NODE ([FSnapshotUtilities nodeFrom:@{ @"foo": @{ @"bar": @YES, @"baz": @"string" }, @"qux": @2, @"quu": @1.2 }])
+
+#define ONE_MEG_NODE ([FTestHelpers leafNodeOfSize:1024*1024])
+#define FIVE_MEG_NODE ([FTestHelpers leafNodeOfSize:5*1024*1024])
+#define TEN_MEG_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024])
+#define TEN_MEG_MINUS_ONE_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024 - 1])
+
+#define SAMPLE_PARAMS \
+ ([[[[[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:PATH(@"child")]] \
+ startAt:[FSnapshotUtilities nodeFrom:@"startVal"] childKey:@"startKey"] \
+ endAt:[FSnapshotUtilities nodeFrom:@"endVal"] childKey:@"endKey"] \
+ limitToLast:5])
+
+#define SAMPLE_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:SAMPLE_PARAMS])
+
+#define DEFAULT_FOO_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:[FQueryParams defaultInstance]])
+
+#define SAMPLE_TRACKED_QUERY \
+ ([[FTrackedQuery alloc] initWithId:1 \
+ query:SAMPLE_QUERY \
+ isPinned:NO \
+ lastUse:100 \
+ Active:NO \
+ isComplete:NO])
+#define OVERWRITE_RECORD(__path, __node, __writeId) \
+ ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] overwrite:__node writeId:__writeId visible:YES])
+
+#define MERGE_RECORD(__path, __merge, __writeId) \
+ ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] merge:__merge writeId:__writeId])
+
+- (void)testUserWriteIsPersisted {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:SAMPLE_NODE atPath:[FPath pathWithString:@"foo/bar"] writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"foo/bar", SAMPLE_NODE, 1)]);
+}
+
+- (void)testUserMergeIsPersisted {
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @{@"bar": @1, @"baz": @"string"}, @"quu": @YES}];
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]);
+}
+
+- (void)testDeepUserMergeIsPersisted {
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @1, @"foo/baz": @"string", @"quu/qux": @YES, @"shallow": @2}];
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]);
+}
+
+- (void)testSameWriteIdOverwritesOldWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:NODE(@"first") atPath:PATH(@"foo/bar") writeId:1];
+ [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1];
+ XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]);
+}
+
+- (void)testHugeWriteWorks {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1];
+ FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:2];
+ NSArray *expected = @[OVERWRITE_RECORD(@"foo/bar", TEN_MEG_NODE, 1), MERGE_RECORD(@"foo/bar", merge, 2)];
+ XCTAssertEqualObjects(engine.userWrites, expected);
+}
+
+- (void)testHugeWritesCanBeDeleted {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1];
+ [engine removeUserWrite:1];
+ XCTAssertTrue(engine.userWrites.count == 0);
+}
+
+- (void)testHugeWritesCanBeInterleavedWithSmallWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2];
+ [engine saveUserOverwrite:NODE(@"node-3") atPath:PATH(@"foo/3") writeId:3];
+ [engine saveUserOverwrite:FIVE_MEG_NODE atPath:PATH(@"foo/4") writeId:4];
+
+ NSArray *expected = @[OVERWRITE_RECORD(@"foo/1", NODE(@"node-1"), 1),
+ OVERWRITE_RECORD(@"foo/2", TEN_MEG_NODE, 2),
+ OVERWRITE_RECORD(@"foo/3", NODE(@"node-3"), 3),
+ OVERWRITE_RECORD(@"foo/4", FIVE_MEG_NODE, 4)];
+ XCTAssertEqualObjects(engine.userWrites, expected);
+}
+
+// This is ported from the Android client and doesn't really make sense since we don't have multi part writes, but
+// It's always good to have tests, so what the heck...
+- (void)testSameWriteIdOverwritesOldMultiPartWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1];
+ [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1];
+
+ XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]);
+}
+
+- (void)testWritesAreReturnedInOrder {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ NSUInteger count = 20;
+ for (NSUInteger i = count - 1; i > 0; i--) {
+ NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i];
+ [engine saveUserOverwrite:NODE(@(i)) atPath:PATH(path) writeId:i];
+ }
+ NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)count];
+ [engine saveUserOverwrite:NODE(@(count)) atPath:PATH(path) writeId:count];
+ NSArray *userWrites = engine.userWrites;
+ XCTAssertEqual(userWrites.count, count);
+ for (NSUInteger i = 1; i <= count; i++) {
+ NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i];
+ XCTAssertEqualObjects(userWrites[i-1], OVERWRITE_RECORD(path, NODE(@(i)), i));
+ }
+}
+
+- (void)testRemoveAllUserWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1];
+ [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2];
+ FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"];
+ [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:3];
+ [engine removeAllUserWrites];
+ XCTAssertEqualObjects(engine.userWrites, @[]);
+}
+
+
+- (void)testCacheSavedIsReturned {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], SAMPLE_NODE);
+}
+
+- (void)testCacheSavedIsReturnedAtRoot {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], SAMPLE_NODE);
+}
+
+- (void)testLaterCacheWritesOverwriteOlderWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO];
+ // this does not affect the node
+ [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO];
+ [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO];
+ [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO];
+
+ id<FNode> expected = [[SAMPLE_NODE updateImmediateChild:@"bar" withNewChild:NODE(@"latest-bar")]
+ updateImmediateChild:@"later-qux" withNewChild:NODE(@"later-qux")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testLaterCacheWritesOverwriteOlderDeeperWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO];
+ // this does not affect the node
+ [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO];
+ [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO];
+ [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO];
+ [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"latest-foo"));
+}
+
+- (void)testLaterCacheWritesDontAffectEarlierWritesAtUnaffectedPath {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO];
+ // this does not affect the node
+ [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO];
+ [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"unaffected")], NODE(@"unaffected"));
+}
+
+- (void)testMergeOnEmptyCacheGivesResults {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ NSDictionary *mergeData = @{@"foo": @"foo-value", @"bar": @"bar-value"};
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:mergeData];
+ [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(mergeData));
+}
+
+- (void)testMergePartlyOverwritingPreviousWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> existingNode = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO];
+
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}];
+ [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testDeepMergePartlyOverwritingPreviousWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> existingNode = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value"}));
+ [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO];
+
+ FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @"new-bar-value", @"quu": @"quu-value"}];
+ [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @{ @"bar": @"new-bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value", @"quu": @"quu-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testMergePartlyOverwritingPreviousMerge {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ FCompoundWrite *merge1 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"foo-value", @"bar": @"bar-value"}];
+ [engine updateServerCacheWithMerge:merge1 atPath:PATH(@"foo")];
+
+ FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}];
+ [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testOverwriteRemovesPreviousMerge {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO];
+
+ FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}];
+ [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")];
+
+ id<FNode> replacingNode = NODE((@{@"qux": @"qux-value", @"quu": @"quu-value"}));
+ [engine updateServerCache:replacingNode atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], replacingNode);
+}
+
+- (void)testEmptyOverwriteDeletesNodeFromHigherWrite {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO];
+
+ // delete bar
+ [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO];
+
+ id<FNode> expected = NODE((@{@"foo": @"foo-value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected);
+}
+
+- (void)testDeeperReadFromHigherSet {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"}));
+ [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/bar")], NODE(@"bar-value"));
+}
+
+- (void)testDeeperLeafNodeSetRemovesHigherLeafNodes {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:NODE(@"level-0") atPath:PATH(@"") merge:NO];
+ [engine updateServerCache:NODE(@"level-1") atPath:PATH(@"lvl1") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], NODE((@{@"lvl1": @"level-1"})));
+
+ [engine updateServerCache:NODE(@"level-2") atPath:PATH(@"lvl1/lvl2") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @"level-2"})));
+
+ [engine updateServerCache:NODE(@"level-4") atPath:PATH(@"lvl1/lvl2/lvl3/lvl4") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @{@"lvl3": @{@"lvl4": @"level-4"}}})));
+}
+
+
+// This test causes a split on Android so it doesn't really make sense here, but why not test anyways...
+- (void)testHugeNodeWithSplit {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ id<FNode> outer = [FEmptyNode emptyNode];
+ // This structure ensures splits at various depths
+ for (NSUInteger i = 0; i < 100; i++) { // Outer
+ id<FNode> inner = [FEmptyNode emptyNode];
+ for (NSUInteger j = 0; j < i; j++) { // Inner
+ id<FNode> innerMost = [FEmptyNode emptyNode];
+ for (NSUInteger k = 0; k < j; k++) {
+ NSString *key = [NSString stringWithFormat:@"key-%lu", (unsigned long)k];
+ id<FNode> node = NODE(([NSString stringWithFormat:@"leaf-%lu", (unsigned long)k]));
+ innerMost = [innerMost updateImmediateChild:key withNewChild:node];
+ }
+ NSString *innerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)j];
+ inner = [inner updateImmediateChild:innerKey withNewChild:innerMost];
+ }
+ NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i];
+ outer = [outer updateImmediateChild:outerKey withNewChild:inner];
+ }
+ [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer);
+}
+
+- (void)testManyLargeLeafNodes {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> outer = [FEmptyNode emptyNode];
+ for (NSUInteger i = 0; i < 30; i++) {
+ NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i];
+ outer = [outer updateImmediateChild:outerKey withNewChild:ONE_MEG_NODE];
+ }
+
+ [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer);
+}
+
+- (void)testPriorityWorks {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine updateServerCache:NODE(@"bar-value") atPath:PATH(@"foo/bar") merge:NO];
+ [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{ @".priority": @"prio-value", @"bar": @"bar-value"})));
+}
+
+- (void)testSimilarSiblingsAreNotLoaded {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine updateServerCache:NODE(@"value") atPath:PATH(@"foo/123") merge:NO];
+ [engine updateServerCache:NODE(@"sibling-value") atPath:PATH(@"foo/1230") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/123")], NODE(@"value"));
+}
+
+// TODO: this test fails, but it is a rare edge case around priorities which would require a bunch of code
+// Fix whenever we have too much time on our hands
+- (void)priorityIsCleared {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ [engine updateServerCache:NODE((@{@"bar": @"bar-value"})) atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO];
+ [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO];
+ [engine updateServerCache:NODE(@"baz-value") atPath:PATH(@"foo/baz") merge:NO];
+
+ // Priority should have been cleaned out
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"baz": @"baz-value"}));
+}
+
+- (void)testHugeLeafNode {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], TEN_MEG_NODE);
+}
+
+- (void)testHugeLeafNodeSiblings {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo/one") merge:NO];
+ [engine updateServerCache:TEN_MEG_MINUS_ONE_NODE atPath:PATH(@"foo/two") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/one")], TEN_MEG_NODE);
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/two")], TEN_MEG_MINUS_ONE_NODE);
+}
+
+- (void)testHugeLeafNodeThenTinyLeafNode {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"tiny") atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"tiny"));
+}
+
+- (void)testHugeLeafNodeThenSmallerLeafNode {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:FIVE_MEG_NODE atPath:PATH(@"foo") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], FIVE_MEG_NODE);
+}
+
+- (void)testHugeLeafNodeThenDeeperSet {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO];
+ [engine updateServerCache:NODE(@"deep-value") atPath:PATH(@"foo/deep") merge:NO];
+
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{@"deep": @"deep-value"})));
+}
+
+// Well this is awkward, but NSJSONSerialization fails to deserialize JSON with tiny/huge doubles
+// It is kind of bad we raise "invalid" data, but at least we don't crash *trollface*
+- (void)testExtremeDoublesAsServerCache {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:NODE((@{@"works": @"value", @"fails": @(2.225073858507201e-308)})) atPath:PATH(@"foo") merge:NO];
+
+ // Will drop the tiny double
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"works": @"value"}));
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/fails")], [FEmptyNode emptyNode]);
+}
+
+- (void)testExtremeDoublesAsTrackedQuery {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> tinyDouble = NODE(@(2.225073858507201e-308));
+
+ FQueryParams *params = [[[FQueryParams defaultInstance] startAt:tinyDouble] endAt:tinyDouble];
+ FTrackedQuery *doesNotWork = [[FTrackedQuery alloc] initWithId:0
+ query:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:params]
+ lastUse:0
+ isActive:NO];
+ FTrackedQuery *doesWork = [[FTrackedQuery alloc] initWithId:1
+ query:[FQuerySpec defaultQueryAtPath:PATH(@"bar")]
+ lastUse:0
+ isActive:NO];
+ [engine saveTrackedQuery:doesNotWork];
+ [engine saveTrackedQuery:doesWork];
+ // One will be dropped, the other should still be there
+ XCTAssertEqualObjects([engine loadTrackedQueries], @[doesWork]);
+}
+
+- (void)testExtremeDoublesAsUserWrites {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ id<FNode> tinyDouble = NODE(@(2.225073858507201e-308));
+
+ [engine saveUserOverwrite:tinyDouble atPath:PATH(@"foo") writeId:1];
+ [engine saveUserMerge:[[FCompoundWrite emptyWrite] addWrite:tinyDouble atPath:PATH(@"bar")] atPath:PATH(@"foo") writeId:2];
+ [engine saveUserOverwrite:NODE(@"should-work") atPath:PATH(@"other") writeId:3];
+
+ // The other two should be dropped and only the valid should remain
+ XCTAssertEqualObjects([engine userWrites], @[[[FWriteRecord alloc] initWithPath:PATH(@"other")
+ overwrite:NODE(@"should-work")
+ writeId:3
+ visible:YES]]);
+}
+
+- (void)testLongValuesDontLosePrecision {
+ id longValue = @1542405709418655810;
+ id floatValue = @2.47;
+ id<FNode> expectedData = NODE((@{@"long": longValue, @"float": floatValue}));
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:expectedData atPath:PATH(@"foo") merge:NO];
+ id<FNode> actualData = [engine serverCacheAtPath:PATH(@"foo")];
+ NSDictionary* value = [actualData val];
+ XCTAssertEqualObjects([value[@"long"] stringValue], [longValue stringValue]);
+ XCTAssertEqualObjects([value[@"float"] stringValue], [floatValue stringValue]);
+}
+
+// NSJSONSerialization has a bug in which it rounds doubles wrongly so hashes end up not matching on the server for
+// some doubles (including 2.47). Make sure LevelDB has the correct hash for that
+- (void)testDoublesAreRoundedProperly {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine updateServerCache:NODE(@(2.47)) atPath:PATH(@"foo") merge:NO];
+
+ // Expected hash for 2.47 parsed correctly
+ NSString *hashFor247 = @"EsibHXKcBp2/b/bn/a0C5WffcUU=";
+ XCTAssertEqualObjects([[engine serverCacheAtPath:PATH(@"foo")] dataHash], hashFor247);
+}
+
+// TODO[offline]: Somehow test estimated server size?
+// TODO[offline]: Test pruning!
+
+- (void)testSaveAndLoadTrackedQueries {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ NSArray *queries = @[[[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO],
+ [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:200 isActive:NO isComplete:NO],
+ [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:300 isActive:YES isComplete:NO],
+ [[FTrackedQuery alloc] initWithId:4 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:400 isActive:NO isComplete:YES],
+ [[FTrackedQuery alloc] initWithId:5 query:[FQuerySpec defaultQueryAtPath:PATH(@"foo")] lastUse:500 isActive:NO isComplete:NO]];
+
+ [queries enumerateObjectsUsingBlock:^(FTrackedQuery *query, NSUInteger idx, BOOL *stop) {
+ [engine saveTrackedQuery:query];
+ }];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], queries);
+}
+
+- (void)testOverwriteTrackedQueryById {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+
+ FTrackedQuery *first = [[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO];
+ FTrackedQuery *second = [[FTrackedQuery alloc] initWithId:1 query:DEFAULT_FOO_QUERY lastUse:200 isActive:YES isComplete:YES];
+ [engine saveTrackedQuery:first];
+ [engine saveTrackedQuery:second];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], @[second]);
+}
+
+- (void)testDeleteTrackedQuery {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO];
+ FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:YES isComplete:NO];
+ FTrackedQuery *query3 = [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:300 isActive:NO isComplete:YES];
+ [engine saveTrackedQuery:query1];
+ [engine saveTrackedQuery:query2];
+ [engine saveTrackedQuery:query3];
+
+ [engine removeTrackedQuery:2];
+ XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query3]));
+}
+
+- (void)testSaveAndLoadTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ NSSet *keys = [NSSet setWithArray:@[@"foo", @"☁", @"10", @"٩(͡๏̯͡๏)۶"]];
+ [engine setTrackedQueryKeys:keys forQueryId:1];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"not", @"included"]] forQueryId:2];
+
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], keys);
+}
+
+- (void)testSaveOverwritesTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]] forQueryId:1];
+
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]]));
+}
+
+- (void)testUpdateTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1];
+ [engine updateTrackedQueryKeysWithAddedKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]]
+ removedKeys:[NSSet setWithArray:@[@"a", @"b"]]
+ forQueryId:1];
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]]));
+}
+
+- (void)testRemoveTrackedQueryRemovesTrackedQueryKeys {
+ FLevelDBStorageEngine *engine = [self cleanStorageEngine];
+ FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO];
+ FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:NO isComplete:NO];
+ [engine saveTrackedQuery:query1];
+ [engine saveTrackedQuery:query2];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b"]] forQueryId:1];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"b", @"c"]] forQueryId:2];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query2]));
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"a", @"b"]]));
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]]));
+
+ [engine removeTrackedQuery:1];
+
+ XCTAssertEqualObjects([engine loadTrackedQueries], (@[query2]));
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], [NSSet set]);
+ XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]]));
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FNodeTests.m b/Example/Database/Tests/Unit/FNodeTests.m
new file mode 100644
index 0000000..372b84f
--- /dev/null
+++ b/Example/Database/Tests/Unit/FNodeTests.m
@@ -0,0 +1,174 @@
+/*
+ * 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 "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FLeafNode.h"
+
+@interface FNodeTests : XCTestCase
+
+@end
+
+@implementation FNodeTests
+
+- (void) testLeafNodeEqualsHashCode {
+ id<FNode> falseNode = [FSnapshotUtilities nodeFrom:@NO];
+ id<FNode> trueNode = [FSnapshotUtilities nodeFrom:@YES];
+ id<FNode> stringOneNode = [FSnapshotUtilities nodeFrom:@"one"];
+ id<FNode> stringTwoNode = [FSnapshotUtilities nodeFrom:@"two"];
+ id<FNode> zeroNode = [FSnapshotUtilities nodeFrom:@0];
+ id<FNode> oneNode = [FSnapshotUtilities nodeFrom:@1];
+ id<FNode> emptyNode1 = [FSnapshotUtilities nodeFrom:nil];
+ id<FNode> emptyNode2 = [FSnapshotUtilities nodeFrom:[NSNull null]];
+
+ XCTAssertEqualObjects(falseNode, [FSnapshotUtilities nodeFrom:@NO]);
+ XCTAssertEqual(falseNode.hash, [FSnapshotUtilities nodeFrom:@NO].hash);
+ XCTAssertEqualObjects(trueNode, [FSnapshotUtilities nodeFrom:@YES]);
+ XCTAssertEqual(trueNode.hash, [FSnapshotUtilities nodeFrom:@YES].hash);
+ XCTAssertFalse([falseNode isEqual:trueNode]);
+ XCTAssertFalse([falseNode isEqual:oneNode]);
+ XCTAssertFalse([falseNode isEqual:stringOneNode]);
+ XCTAssertFalse([falseNode isEqual:emptyNode1]);
+
+ XCTAssertEqualObjects(stringOneNode, [FSnapshotUtilities nodeFrom:@"one"]);
+ XCTAssertEqual(stringOneNode.hash, [FSnapshotUtilities nodeFrom:@"one"].hash);
+ XCTAssertFalse([stringOneNode isEqual:stringTwoNode]);
+ XCTAssertFalse([stringOneNode isEqual:emptyNode1]);
+ XCTAssertFalse([stringOneNode isEqual:oneNode]);
+ XCTAssertFalse([stringOneNode isEqual:trueNode]);
+
+ XCTAssertEqualObjects(zeroNode, [FSnapshotUtilities nodeFrom:@0]);
+ XCTAssertEqual(zeroNode.hash, [FSnapshotUtilities nodeFrom:@0].hash);
+ XCTAssertFalse([zeroNode isEqual:oneNode]);
+ XCTAssertFalse([zeroNode isEqual:emptyNode1]);
+ XCTAssertFalse([zeroNode isEqual:falseNode]);
+
+ XCTAssertEqualObjects(emptyNode1, emptyNode2);
+ XCTAssertEqual(emptyNode1.hash, emptyNode2.hash);
+}
+
+- (void) testLeafNodePrioritiesEqualsHashCode {
+ id<FNode> oneOne = [FSnapshotUtilities nodeFrom:@1 priority:@1];
+ id<FNode> stringOne = [FSnapshotUtilities nodeFrom:@"value" priority:@1];
+ id<FNode> oneString = [FSnapshotUtilities nodeFrom:@1 priority:@"value"];
+ id<FNode> stringString = [FSnapshotUtilities nodeFrom:@"value" priority:@"value"];
+
+ XCTAssertEqualObjects(oneOne, [FSnapshotUtilities nodeFrom:@1 priority:@1]);
+ XCTAssertEqual(oneOne.hash, [FSnapshotUtilities nodeFrom:@1 priority:@1].hash);
+ XCTAssertFalse([oneOne isEqual:stringOne]);
+ XCTAssertFalse([oneOne isEqual:oneString]);
+ XCTAssertFalse([oneOne isEqual:stringString]);
+
+ XCTAssertEqualObjects(stringOne, [FSnapshotUtilities nodeFrom:@"value" priority:@1]);
+ XCTAssertEqual(stringOne.hash, [FSnapshotUtilities nodeFrom:@"value" priority:@1].hash);
+ XCTAssertFalse([stringOne isEqual:oneOne]);
+ XCTAssertFalse([stringOne isEqual:oneString]);
+ XCTAssertFalse([stringOne isEqual:stringString]);
+
+ XCTAssertEqualObjects(oneString, [FSnapshotUtilities nodeFrom:@1 priority:@"value"]);
+ XCTAssertEqual(oneString.hash, [FSnapshotUtilities nodeFrom:@1 priority:@"value"].hash);
+ XCTAssertFalse([oneString isEqual:stringOne]);
+ XCTAssertFalse([oneString isEqual:oneOne]);
+ XCTAssertFalse([oneString isEqual:stringString]);
+
+ XCTAssertEqualObjects(stringString, [FSnapshotUtilities nodeFrom:@"value" priority:@"value"]);
+ XCTAssertEqual(stringString.hash, [FSnapshotUtilities nodeFrom:@"value" priority:@"value"].hash);
+ XCTAssertFalse([stringString isEqual:stringOne]);
+ XCTAssertFalse([stringString isEqual:oneString]);
+ XCTAssertFalse([stringString isEqual:oneOne]);
+}
+
+- (void)testChildrenNodeEqualsHashCode {
+ id<FNode> nodeOne = [FSnapshotUtilities nodeFrom:@{ @"one": @1, @"two": @2, @".priority": @"prio"}];
+ id<FNode> nodeTwo = [[FEmptyNode emptyNode] updateImmediateChild:@"one" withNewChild:[FSnapshotUtilities nodeFrom:@1]];
+ nodeTwo = [nodeTwo updateImmediateChild:@"two" withNewChild:[FSnapshotUtilities nodeFrom:@2]];
+ nodeTwo = [nodeTwo updatePriority:[FSnapshotUtilities nodeFrom:@"prio"]];
+
+ XCTAssertEqualObjects(nodeOne, nodeTwo);
+ XCTAssertEqual(nodeOne.hash, nodeTwo.hash);
+ XCTAssertFalse([[nodeOne updatePriority:[FEmptyNode emptyNode]] isEqual:nodeOne]);
+ XCTAssertFalse([[nodeOne updateImmediateChild:@"one" withNewChild:[FEmptyNode emptyNode]] isEqual:nodeOne]);
+ XCTAssertFalse([[nodeOne updateImmediateChild:@"one" withNewChild:[FSnapshotUtilities nodeFrom:@2]] isEqual:nodeOne]);
+}
+
+- (void)testLeadingZerosWorkCorrectly {
+ NSDictionary *data = @{ @"1": @1, @"01": @2, @"001": @3, @"0001": @4 };
+
+ id<FNode> node = [FSnapshotUtilities nodeFrom:data];
+ XCTAssertEqualObjects([node getImmediateChild:@"1"].val, @1);
+ XCTAssertEqualObjects([node getImmediateChild:@"01"].val, @2);
+ XCTAssertEqualObjects([node getImmediateChild:@"001"].val, @3);
+ XCTAssertEqualObjects([node getImmediateChild:@"0001"].val, @4);
+}
+
+- (void)testLeadindZerosArePreservedInValue {
+ NSDictionary *data = @{ @"1": @1, @"01": @2, @"001": @3, @"0001": @4 };
+
+ XCTAssertEqualObjects([FSnapshotUtilities nodeFrom:data].val, data);
+}
+
+- (void)testEmptyNodeEqualsEmptyChildrenNode {
+ XCTAssertEqualObjects([FEmptyNode emptyNode], [[FChildrenNode alloc] init]);
+ XCTAssertEqualObjects([[FChildrenNode alloc] init], [FEmptyNode emptyNode]);
+ XCTAssertEqual([[FChildrenNode alloc] init].hash, [FEmptyNode emptyNode].hash);
+}
+
+- (void)testUpdatingEmptyChildrenDoesntOverwriteLeafNode {
+ FLeafNode *node = [[FLeafNode alloc] initWithValue:@"value"];
+ XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@".priority"] withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@"child"] withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@"child/.priority"] withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@"child" withNewChild:[FEmptyNode emptyNode]]);
+ XCTAssertEqualObjects(node, [node updateImmediateChild:@".priority" withNewChild:[FEmptyNode emptyNode]]);
+}
+
+- (void)testUpdatingPrioritiesOnEmptyNodesIsANoOp {
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:@"prio"];
+ XCTAssertTrue([[[[FEmptyNode emptyNode] updatePriority:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[[FEmptyNode emptyNode] updateChild:[FPath pathWithString:@".priority"] withNewChild:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[[FEmptyNode emptyNode] updateImmediateChild:@".priority" withNewChild:priority] getPriority] isEmpty]);
+
+ id<FNode> valueNode = [FSnapshotUtilities nodeFrom:@"value"];
+ FPath *childPath = [FPath pathWithString:@"child"];
+ id<FNode> reemptiedChildren = [[[FEmptyNode emptyNode] updateChild:childPath withNewChild:valueNode] updateChild:childPath withNewChild:[FEmptyNode emptyNode]];
+ XCTAssertTrue([[[reemptiedChildren updatePriority:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[reemptiedChildren updateChild:[FPath pathWithString:@".priority"] withNewChild:priority] getPriority] isEmpty]);
+ XCTAssertTrue([[[reemptiedChildren updateImmediateChild:@".priority" withNewChild:priority] getPriority] isEmpty]);
+}
+
+- (void)testDeletingLastChildFromChildrenNodeRemovesPriority {
+ id<FNode> priority = [FSnapshotUtilities nodeFrom:@"prio"];
+ id<FNode> valueNode = [FSnapshotUtilities nodeFrom:@"value"];
+ FPath *childPath = [FPath pathWithString:@"child"];
+ id<FNode> withPriority = [[[FEmptyNode emptyNode] updateChild:childPath withNewChild:valueNode] updatePriority:priority];
+ XCTAssertEqualObjects(priority, [withPriority getPriority]);
+ id<FNode> deletedChild = [withPriority updateChild:childPath withNewChild:[FEmptyNode emptyNode]];
+ XCTAssertTrue([[deletedChild getPriority] isEmpty]);
+}
+
+- (void)testFromNodeReturnsEmptyNodesWithoutPriority {
+ id<FNode> empty1 = [FSnapshotUtilities nodeFrom:@{ @".priority": @"prio" }];
+ XCTAssertTrue([[empty1 getPriority] isEmpty]);
+
+ id<FNode> empty2 = [FSnapshotUtilities nodeFrom:@{ @"dummy": [NSNull null], @".priority": @"prio" }];
+ XCTAssertTrue([[empty2 getPriority] isEmpty]);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FPathTests.h b/Example/Database/Tests/Unit/FPathTests.h
new file mode 100644
index 0000000..edd8330
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPathTests.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FPathTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FPathTests.m b/Example/Database/Tests/Unit/FPathTests.m
new file mode 100644
index 0000000..9b26a85
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPathTests.m
@@ -0,0 +1,84 @@
+/*
+ * 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 "FPathTests.h"
+#import "FPath.h"
+
+@implementation FPathTests
+
+- (void)testContains
+{
+ XCTAssertTrue([[[FPath alloc] initWith:@"/"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWith:@"/a"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWith:@"/a/b"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/b"]], @"contains should be correct");
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a"]], @"contains should be correct");
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/"]], @"contains should be correct");
+
+ NSArray *pathPieces = @[@"a",@"b",@"c"];
+
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c/d"]], @"contains should be correct");
+
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct");
+ XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/c/b"]], @"contains should be correct");
+
+ XCTAssertFalse([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1]contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct");
+ XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c/d"]], @"contains should be correct");
+}
+
+- (void)testPopFront
+{
+ XCTAssertEqualObjects([[[FPath alloc] initWith:@"/a/b/c"] popFront], [[FPath alloc] initWith:@"/b/c"], @"should be correct");
+ XCTAssertEqualObjects([[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront], [[FPath alloc] initWith:@"/c"], @"should be correct");
+ XCTAssertEqualObjects([[[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront] popFront], [[FPath alloc] initWith:@"/"], @"should be correct");
+ XCTAssertEqualObjects([[[[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront] popFront] popFront], [[FPath alloc] initWith:@"/"], @"should be correct");
+}
+
+- (void)testParent
+{
+ XCTAssertEqualObjects([[[FPath alloc] initWith:@"/a/b/c"] parent], [[FPath alloc] initWith:@"/a/b/"], @"should be correct");
+ XCTAssertEqualObjects([[[[FPath alloc] initWith:@"/a/b/c"] parent] parent], [[FPath alloc] initWith:@"/a/"], @"should be correct");
+ XCTAssertEqualObjects([[[[[FPath alloc] initWith:@"/a/b/c"] parent] parent] parent], [[FPath alloc] initWith:@"/"], @"should be correct");
+ XCTAssertNil([[[[[[FPath alloc] initWith:@"/a/b/c"] parent] parent] parent] parent], @"should be correct");
+}
+
+- (void)testWireFormat
+{
+ XCTAssertEqualObjects(@"/", [[FPath empty] wireFormat]);
+ XCTAssertEqualObjects(@"a/b/c", [[[FPath alloc] initWith:@"/a/b//c/"] wireFormat]);
+ XCTAssertEqualObjects(@"b/c", [[[[FPath alloc] initWith:@"/a/b//c/"] popFront] wireFormat]);
+}
+
+- (void)testComparison
+{
+ NSArray *pathsInOrder = @[@"1", @"2", @"10", @"a", @"a/1", @"a/2", @"a/10", @"a/a", @"a/aa", @"a/b", @"a/b/c",
+ @"b", @"b/a"];
+ for (NSInteger i = 0; i < pathsInOrder.count; i++) {
+ FPath *path1 = PATH(pathsInOrder[i]);
+ for (NSInteger j = i + 1; j < pathsInOrder.count; j++) {
+ FPath *path2 = PATH(pathsInOrder[j]);
+ XCTAssertEqual([path1 compare:path2], NSOrderedAscending);
+ XCTAssertEqual([path2 compare:path1], NSOrderedDescending);
+ }
+ XCTAssertEqual([path1 compare:path1], NSOrderedSame);
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FPersistenceManagerTest.m b/Example/Database/Tests/Unit/FPersistenceManagerTest.m
new file mode 100644
index 0000000..c00d11f
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPersistenceManagerTest.m
@@ -0,0 +1,106 @@
+/*
+ * 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 <XCTest/XCTest.h>
+#import "FPersistenceManager.h"
+#import "FTestCachePolicy.h"
+#import "FMockStorageEngine.h"
+#import "FTestHelpers.h"
+#import "FQuerySpec.h"
+#import "FSnapshotUtilities.h"
+#import "FPathIndex.h"
+#import "FIndexedNode.h"
+#import "FEmptyNode.h"
+
+@interface FPersistenceManagerTest : XCTestCase
+
+@end
+
+@implementation FPersistenceManagerTest
+
+- (FPersistenceManager *)newTestPersistenceManager {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FPersistenceManager *manager = [[FPersistenceManager alloc] initWithStorageEngine:engine
+ cachePolicy:[FNoCachePolicy noCachePolicy]];
+ return manager;
+}
+
+- (void)testServerCacheFiltersResults1 {
+ FPersistenceManager *manager = [self newTestPersistenceManager];
+
+ [manager updateServerCacheWithNode:NODE(@"1") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]];
+ [manager updateServerCacheWithNode:NODE(@"2") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/baz")]];
+ [manager updateServerCacheWithNode:NODE(@"3") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/quu/1")]];
+ [manager updateServerCacheWithNode:NODE(@"4") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/quu/2")]];
+
+ FCacheNode *cache = [manager serverCacheForQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ XCTAssertFalse(cache.isFullyInitialized);
+ XCTAssertEqualObjects(cache.node, [FEmptyNode emptyNode]);
+}
+
+- (void)testServerCacheFiltersResults2 {
+ FPersistenceManager *manager = [self newTestPersistenceManager];
+
+ FQuerySpec *limit2FooQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo") params:[[FQueryParams defaultInstance] limitToFirst:2]];
+ FQuerySpec *limit3FooQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo") params:[[FQueryParams defaultInstance] limitToFirst:3]];
+
+ [manager setQueryActive:limit2FooQuery];
+ [manager updateServerCacheWithNode:NODE((@{@"a": @1, @"b": @2, @"c": @3, @"d": @4})) forQuery:limit2FooQuery];
+ [manager setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b"]] forQuery:limit2FooQuery];
+
+ FCacheNode *cache = [manager serverCacheForQuery:limit3FooQuery];
+ XCTAssertFalse(cache.isFullyInitialized);
+ XCTAssertEqualObjects(cache.node, NODE((@{@"a": @1, @"b": @2})));
+}
+
+- (void)testNoLimitNonDefaultQueryIsTreatedAsDefaultQuery {
+ FPersistenceManager *manager = [self newTestPersistenceManager];
+
+ FQuerySpec *defaultQuery = [FQuerySpec defaultQueryAtPath:PATH(@"foo")];
+ id<FIndex> index = [[FPathIndex alloc] initWithPath:PATH(@"index-key")];
+ FQuerySpec *orderByQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo")
+ params:[[FQueryParams defaultInstance] orderBy:index]];
+ [manager setQueryActive:defaultQuery];
+ [manager updateServerCacheWithNode:NODE((@{@"foo": @1, @"bar": @2}))
+ forQuery:defaultQuery];
+ [manager setQueryComplete:defaultQuery];
+
+ FCacheNode *node = [manager serverCacheForQuery:orderByQuery];
+
+ XCTAssertEqualObjects(node.node, NODE((@{@"foo": @1, @"bar": @2})));
+ XCTAssertTrue(node.isFullyInitialized);
+ XCTAssertFalse(node.isFiltered);
+ XCTAssertTrue([node.indexedNode hasIndex:orderByQuery.index]);
+}
+
+- (void)testApplyUserMergeUsesRelativePath {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+
+ id<FNode> initialData = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"baz-value"}}));
+ [engine updateServerCache:initialData atPath:PATH(@"") merge:NO];
+
+ FPersistenceManager *manager = [[FPersistenceManager alloc] initWithStorageEngine:engine
+ cachePolicy:[FNoCachePolicy noCachePolicy]];
+
+ FCompoundWrite *update = [FCompoundWrite compoundWriteWithValueDictionary:@{@"baz": @"new-baz", @"qux": @"qux"}];
+ [manager applyUserMerge:update toServerCacheAtPath:PATH(@"foo")];
+
+ id<FNode> expected = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"new-baz", @"qux": @"qux"}}));
+ id<FNode> actual = [engine serverCacheAtPath:PATH(@"")];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FPruneForestTest.m b/Example/Database/Tests/Unit/FPruneForestTest.m
new file mode 100644
index 0000000..0694ba7
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPruneForestTest.m
@@ -0,0 +1,98 @@
+/*
+ * 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 "FPruneForest.h"
+#import "FPath.h"
+
+@interface FPruneForestTest : XCTestCase
+
+@end
+
+@implementation FPruneForestTest
+
+- (void) testEmptyDoesNotAffectAnyPaths {
+ FPruneForest *forest = [FPruneForest empty];
+ XCTAssertFalse([forest affectsPath:[FPath empty]]);
+ XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"foo"]]);
+}
+
+- (void) testPruneAffectsPath {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo/bar"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]];
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo"]]);
+ XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"baz"]]);
+ XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"baz/bar"]]);
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar"]]);
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar/baz"]]);
+ XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar/qux"]]);
+}
+
+- (void) testPruneAnythingWorks {
+ FPruneForest *empty = [FPruneForest empty];
+ XCTAssertFalse([empty prunesAnything]);
+ XCTAssertTrue([[empty prunePath:[FPath pathWithString:@"foo"]] prunesAnything]);
+ XCTAssertFalse([[[empty prunePath:[FPath pathWithString:@"foo/bar"]] keepPath:[FPath pathWithString:@"foo"]] prunesAnything]);
+ XCTAssertTrue([[[empty prunePath:[FPath pathWithString:@"foo"]] keepPath:[FPath pathWithString:@"foo/bar"]] prunesAnything]);
+}
+
+- (void) testKeepUnderPruneWorks {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo/bar"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]];
+ forest = [forest keepAll:[NSSet setWithArray:@[@"qux", @"quu"]] atPath:[FPath pathWithString:@"foo/bar"]];
+}
+
+- (void) testPruneUnderKeepThrows {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar"]];
+ XCTAssertThrows([forest prunePath:[FPath pathWithString:@"foo/bar/baz"]]);
+ NSSet *children = [NSSet setWithArray:@[@"qux", @"quu"]];
+ XCTAssertThrows([forest pruneAll:children atPath:[FPath pathWithString:@"foo/bar"]]);
+}
+
+- (void) testChildKeepsPruneInfo {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar"]];
+ XCTAssertTrue([[forest child:@"foo"] affectsPath:[FPath pathWithString:@"bar"]]);
+ XCTAssertTrue([[[forest child:@"foo"] child:@"bar"] affectsPath:[FPath pathWithString:@""]]);
+ XCTAssertTrue([[[[forest child:@"foo"] child:@"bar"] child:@"baz"] affectsPath:[FPath pathWithString:@""]]);
+
+ forest = [[FPruneForest empty] prunePath:[FPath pathWithString:@"foo/bar"]];
+ XCTAssertTrue([[forest child:@"foo"] affectsPath:[FPath pathWithString:@"bar"]]);
+ XCTAssertTrue([[[forest child:@"foo"] child:@"bar"] affectsPath:[FPath pathWithString:@""]]);
+ XCTAssertTrue([[[[forest child:@"foo"] child:@"bar"] child:@"baz"] affectsPath:[FPath pathWithString:@""]]);
+
+ XCTAssertFalse([[forest child:@"non-existent"] affectsPath:[FPath pathWithString:@""]]);
+}
+
+- (void) testShouldPruneWorks {
+ FPruneForest *forest = [FPruneForest empty];
+ forest = [forest prunePath:[FPath pathWithString:@"foo"]];
+ forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]];
+ XCTAssertTrue([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo"]]);
+ XCTAssertTrue([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo/bar"]]);
+ XCTAssertFalse([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo/bar/baz"]]);
+ XCTAssertFalse([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"qux"]]);
+}
+
+
+@end
diff --git a/Example/Database/Tests/Unit/FPruningTest.m b/Example/Database/Tests/Unit/FPruningTest.m
new file mode 100644
index 0000000..d1e7354
--- /dev/null
+++ b/Example/Database/Tests/Unit/FPruningTest.m
@@ -0,0 +1,293 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+#import "FLevelDBStorageEngine.h"
+#import "FTestHelpers.h"
+#import "FPruneForest.h"
+#import "FEmptyNode.h"
+#import "FMockStorageEngine.h"
+
+@interface FPruningTest : XCTestCase
+
+@end
+
+static id<FNode> ABC_NODE = nil;
+static id<FNode> DEF_NODE = nil;
+static id<FNode> A_NODE = nil;
+static id<FNode> D_NODE = nil;
+static id<FNode> BC_NODE = nil;
+static id<FNode> LARGE_NODE = nil;
+
+@implementation FPruningTest
+
++ (void)initStatics {
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ ABC_NODE = NODE((@{@"a": @{@"aa": @1.1, @"ab": @1.2}, @"b": @2, @"c": @3}));
+ DEF_NODE = NODE((@{@"d": @4, @"e": @5, @"f": @6}));
+ A_NODE = NODE((@{@"a": @{@"aa": @1.1, @"ab": @1.2}}));
+ D_NODE = NODE(@{@"d": @4});
+ LARGE_NODE = [FTestHelpers leafNodeOfSize:5*1024*1024];
+ BC_NODE = [ABC_NODE updateImmediateChild:@"a" withNewChild:[FEmptyNode emptyNode]];
+ });
+}
+
+- (void)runWithDb:(void (^)(id<FStorageEngine>engine))block {
+ [FPruningTest initStatics];
+ {
+ // Run with level DB implementation
+ FLevelDBStorageEngine *engine = [[FLevelDBStorageEngine alloc] initWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"purge-tests"]];
+ block(engine);
+ [engine purgeEverything];
+ [engine close];
+ }
+ {
+ // Run with mock implementation
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ block(engine);
+ [engine close];
+
+ }
+}
+
+- (FPruneForest *)prune:(NSString *)pathStr {
+ return [[FPruneForest empty] prunePath:PATH(pathStr)];
+}
+
+- (FPruneForest *)prune:(NSString *)path exceptRelative:(NSArray *)except {
+ __block FPruneForest *pruneForest = [FPruneForest empty];
+ pruneForest = [pruneForest prunePath:PATH(path)];
+ [except enumerateObjectsUsingBlock:^(NSString *keepPath, NSUInteger idx, BOOL *stop) {
+ pruneForest = [pruneForest keepPath:[PATH(path) childFromString:keepPath]];
+ }];
+ return pruneForest;
+}
+
+// Write document at root, prune it.
+- (void)test010 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x, prune it via PruneForest for /x, at root.
+- (void)test020 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@"x"] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x, prune it via PruneForest for root, at /x.
+- (void)test030 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x, prune it via PruneForest for root, at root
+- (void)test040 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write document at /x/y, prune it via PruneForest for /y, at /x
+- (void)test050 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"y"] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at /x/y, prune /x/y except b,c via PruneForest for /x/y -b,c, at root
+- (void)test060 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"x/y" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], BC_NODE);
+ }];
+}
+
+// Write abc at /x/y, prune /x/y except b,c via PruneForest for /y -b,c, at /x
+- (void)test070 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"y" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], BC_NODE);
+ }];
+}
+
+// Write abc at /x/y, prune /x/y except not-there via PruneForest for /x/y -d, at root
+- (void)test080 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO];
+ [engine pruneCache:[self prune:@"x/y" exceptRelative:@[@"not-there"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at / and def at /a, prune all via PruneForest for / at root
+- (void)test090 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"a") merge:NO];
+ [engine pruneCache:[self prune:@""] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at / and def at /a, prune all except b,c via PruneForest for root -b,c, at root
+- (void)test100 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], BC_NODE);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except b,c via PruneForest for /x -b,c, at root
+- (void)test110 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], BC_NODE);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except b,c via PruneForest for root -b,c, at /x
+- (void)test120 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], BC_NODE);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a via PruneForest for /x -a, at root
+- (void)test130 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [ABC_NODE updateImmediateChild:@"a" withNewChild:DEF_NODE]);
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a via PruneForest for root -a, at /x
+- (void)test140 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a/d via PruneForest for /x -a/d, at root
+- (void)test150 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a/d"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:D_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a, prune /x except a/d via PruneForest for / -a/d, at /x
+- (void)test160 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/d"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:D_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a via PruneForest for /x -a, at root
+- (void)test170 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [A_NODE updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a via PruneForest for / -a, at /x
+- (void)test180 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/aa"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a/aa via PruneForest for /x -a/aa, at root
+- (void)test190 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a/aa"]] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write abc at /x and def at /x/a/aa, prune /x except a/aa via PruneForest for / -a/aa, at /x
+- (void)test200 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/aa"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]);
+ }];
+}
+
+// Write large node at /x, prune x via PruneForest for x at root
+- (void)test210 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:LARGE_NODE atPath:PATH(@"x") merge:NO];
+ [engine pruneCache:[self prune:@"x"] atPath:PATH(@"")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]);
+ }];
+}
+
+// Write abc at x and large node at /x/a, prune x except a via PruneForest for / -a, at x
+- (void)test220 {
+ [self runWithDb:^(id<FStorageEngine> engine) {
+ [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO];
+ [engine updateServerCache:LARGE_NODE atPath:PATH(@"x/a") merge:NO];
+ [engine pruneCache:[self prune:@"" exceptRelative:@[@"a"]] atPath:PATH(@"x")];
+ XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:LARGE_NODE]);
+ }];
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FQueryParamsTest.m b/Example/Database/Tests/Unit/FQueryParamsTest.m
new file mode 100644
index 0000000..8c98ff9
--- /dev/null
+++ b/Example/Database/Tests/Unit/FQueryParamsTest.m
@@ -0,0 +1,162 @@
+/*
+ * 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 "FQueryParams.h"
+#import "FIndex.h"
+#import "FPriorityIndex.h"
+#import "FValueIndex.h"
+#import "FLeafNode.h"
+#import "FPathIndex.h"
+#import "FSnapshotUtilities.h"
+#import "FKeyIndex.h"
+#import "FEmptyNode.h"
+
+@interface FQueryParamsTest : XCTestCase
+
+@end
+
+@implementation FQueryParamsTest
+
+- (void)testQueryParamsEquals {
+ { // Limit equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] limitToLast:10];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] limitTo:10];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] limitToFirst:10];
+ FQueryParams *params4 = [[FQueryParams defaultInstance] limitToLast:11];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ XCTAssertFalse([params1 isEqual:params4]);
+ }
+
+ { // Index equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] orderBy:[FKeyIndex keyIndex]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // startAt equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value"] childKey:nil];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value-2"]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // startAt with childkey equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"other-key"];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // endAt equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value"] childKey:nil];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value-2"]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // endAt with childkey equals
+ FQueryParams *params1 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"key"];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"other-key"];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+
+ { // Limit/startAt equals
+ FQueryParams *params1 = [[[FQueryParams defaultInstance] limitToFirst:10] startAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params2 = [[[FQueryParams defaultInstance] limitTo:10] startAt:[FSnapshotUtilities nodeFrom:@"value"]];
+ FQueryParams *params3 = [[[FQueryParams defaultInstance] limitTo:10] startAt:[FSnapshotUtilities nodeFrom:@"value-2"]];
+ XCTAssertEqualObjects(params1, params2);
+ XCTAssertEqual(params1.hash, params2.hash);
+ XCTAssertFalse([params1 isEqual:params3]);
+ }
+}
+
+- (void)testFromDictionaryEquals {
+ FQueryParams *params1 = [[[[[FQueryParams defaultInstance] limitToLast:10]
+ startAt:[FSnapshotUtilities nodeFrom:@"start-value"] childKey:@"child-key-2"]
+ endAt:[FSnapshotUtilities nodeFrom:@"end-value"] childKey:@"child-key-2"]
+ orderBy:[FKeyIndex keyIndex]];
+ XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]);
+ XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash);
+}
+
+- (void)testCanCreateAllIndexes {
+ FQueryParams *params1 = [[FQueryParams defaultInstance] orderBy:[FKeyIndex keyIndex]];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] orderBy:[FValueIndex valueIndex]];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]];
+ FQueryParams *params4 = [[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:[[FPath alloc] initWith:@"subkey"]]];
+ XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]);
+ XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params2.wireProtocolParams]);
+ XCTAssertEqualObjects(params3, [FQueryParams fromQueryObject:params3.wireProtocolParams]);
+ XCTAssertEqualObjects(params4, [FQueryParams fromQueryObject:params4.wireProtocolParams]);
+ XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash);
+ XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params2.wireProtocolParams].hash);
+ XCTAssertEqual(params3.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash);
+ XCTAssertEqual(params4.hash, [FQueryParams fromQueryObject:params4.wireProtocolParams].hash);
+}
+
+- (void)testDifferentLimits {
+ FQueryParams *params1 = [[FQueryParams defaultInstance] limitToFirst:10];
+ FQueryParams *params2 = [[FQueryParams defaultInstance] limitToLast:10];
+ FQueryParams *params3 = [[FQueryParams defaultInstance] limitTo:10];
+ XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]);
+ XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params2.wireProtocolParams]);
+ XCTAssertEqualObjects(params3, [FQueryParams fromQueryObject:params3.wireProtocolParams]);
+ // 2 and 3 are equivalent
+ XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params3.wireProtocolParams]);
+
+ XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash);
+ XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params2.wireProtocolParams].hash);
+ XCTAssertEqual(params3.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash);
+ // 2 and 3 are equivalent
+ XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash);
+}
+
+- (void)testStartAtNullIsSerializable {
+ FQueryParams *params = [FQueryParams defaultInstance];
+ params = [params startAt:[FEmptyNode emptyNode] childKey:@"key"];
+ NSDictionary *dict = [params wireProtocolParams];
+ FQueryParams *parsed = [FQueryParams fromQueryObject:dict];
+ XCTAssertEqualObjects(parsed, params);
+ XCTAssertTrue([parsed hasStart]);
+}
+
+- (void)testEndAtNullIsSerializable {
+ FQueryParams *params = [FQueryParams defaultInstance];
+ params = [params endAt:[FEmptyNode emptyNode] childKey:@"key"];
+ NSDictionary *dict = [params wireProtocolParams];
+ FQueryParams *parsed = [FQueryParams fromQueryObject:dict];
+ XCTAssertEqualObjects(parsed, params);
+ XCTAssertTrue([parsed hasEnd]);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FRangeMergeTest.m b/Example/Database/Tests/Unit/FRangeMergeTest.m
new file mode 100644
index 0000000..32ea6ad
--- /dev/null
+++ b/Example/Database/Tests/Unit/FRangeMergeTest.m
@@ -0,0 +1,271 @@
+/*
+ * 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 "FRangeMerge.h"
+#import "FNode.h"
+#import "FTestHelpers.h"
+#import "FEmptyNode.h"
+
+@interface FRangeMergeTest : XCTestCase
+
+@end
+
+@implementation FRangeMergeTest
+
+- (void)testSmokeTest {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @{@"a": @{@"deep-a-1": @1, @"deep-a-2": @2}, @"b": @"b", @"c": @"c", @"d": @"d"}, @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"a": @{@"deep-a-2": @"new-a-2", @"deep-a-3": @3}, @"b-2": @"new-b", @"c": @"new-c" }}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo/a/deep-a-1") end:PATH(@"foo/c") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @{@"a": @{@"deep-a-1": @1, @"deep-a-2": @"new-a-2", @"deep-a-3": @3}, @"b-2": @"new-b", @"c": @"new-c", @"d": @"d"}, @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testStartIsExclusive {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"foo": @"new-foo-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @"new-foo-value", @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testStartIsExclusiveButIncludesChildren {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"bar": @{@"bar-child": @"bar-child-value"}, @"foo": @"new-foo-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @{@"bar-child": @"bar-child-value"}, @"foo": @"new-foo-value", @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testEndIsInclusive {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"baz": @"baz-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; // foo should be deleted
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"baz": @"baz-value", @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testEndIsInclusiveButExcludesChildren {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @{@"foo-child": @"foo-child-value"}, @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"baz": @"baz-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; // foo should be deleted
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"baz": @"baz-value", @"foo": @{@"foo-child": @"foo-child-value"}, @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testCanUpdateLeafNode {
+ id<FNode> node = NODE(@"leaf-value");
+
+ id<FNode> updates = NODE((@{@"bar": @"bar-value"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo") updates:updates];
+ id<FNode> expected = NODE((@{@"bar": @"bar-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testCanReplaceLeafNodeWithLeafNode{
+ id<FNode> node = NODE(@"leaf-value");
+
+ id<FNode> updates = NODE(@"new-leaf-value");
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"") updates:updates];
+ id<FNode> expected = NODE(@"new-leaf-value");
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testLeafsAreUpdatedWhenRangesIncludeDeeperPath {
+ id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"bar": @"new-bar-value"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/bar/deep") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"bar": @"new-bar-value"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testLeafsAreNotUpdatedWhenRangesIncludeDeeperPaths {
+ id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"bar": @"new-bar-value"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo/bar") end:PATH(@"foo/bar/deep") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingEntireRangeUpdatesEverything {
+ id<FNode> node = [FEmptyNode emptyNode];
+
+ id<FNode> updates = NODE((@{@"foo": @"foo-value", @"bar": @{@"child": @"bar-child-value"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:nil updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @"foo-value", @"bar": @{@"child": @"bar-child-value"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingRangeWithUnboundedLeftPostWorks {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"}));
+
+ id<FNode> updates = NODE((@{@"bar": @"new-bar"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingRangeWithRightPostChildOfLeftPostWorks {
+ id<FNode> node = NODE((@{@"foo": @{@"a": @"a", @"b": @{@"1": @"1", @"2": @"2"}, @"c": @"c"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1"}}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/b/1") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1", @"2": @"2"}, @"c": @"c"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingRangeWithRightPostChildOfLeftPostWorksWithIntegerKeys {
+ id<FNode> node = NODE((@{@"foo": @{@"a": @"a", @"b": @{@"1": @"1", @"2": @"2", @"10": @"10"}, @"c": @"c"}}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1"}}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/b/2") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1", @"10": @"10"}, @"c": @"c"}}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingLeafIncludesPriority {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"}));
+
+ id<FNode> updates = NODE((@{@"foo": @{@".value": @"new-foo", @".priority": @"prio"}}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @{@".value": @"new-foo", @".priority": @"prio" }, @"quu": @"quu-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingPriorityInChildrenNodeWorks {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"}));
+
+ id<FNode> updates = NODE((@{@"bar": @"new-bar", @".priority": @"prio"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+// TODO: this test should actuall;y work, but priorities on empty nodes are ignored :(
+- (void)updatingPriorityInChildrenNodeWorksAlone {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"}));
+
+ id<FNode> updates = NODE((@{@".priority": @"prio" }));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@".priority") updates:updates];
+
+ id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testUpdatingPriorityOnInitiallyEmptyNodeDoesNotBreak {
+ id<FNode> node = NODE((@{}));
+
+ id<FNode> updates = NODE((@{@".priority": @"prio", @"foo": @"foo-value" }));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo") updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @"foo-value", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testPriorityIsDeletedWhenIncludedInChildrenRange {
+ id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @".priority": @"prio"}));
+
+ id<FNode> updates = NODE((@{@"bar": @"new-bar"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates]; // deletes priority
+
+ id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testPriorityIsIncludedInOpenStart {
+ id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}}));
+
+ id<FNode> updates = NODE((@{@".priority": @"prio", @"baz": @"baz"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo/bar") updates:updates];
+
+ id<FNode> expected = NODE((@{@"baz": @"baz", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testPriorityIsIncludedInOpenEnd {
+ id<FNode> node = NODE(@"leaf-node");
+
+ id<FNode> updates = NODE((@{@".priority": @"prio", @"foo": @"bar"}));
+
+ FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"/") end:nil updates:updates];
+
+ id<FNode> expected = NODE((@{@"foo": @"bar", @".priority": @"prio"}));
+ id<FNode> actual = [merge applyToNode:node];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FRepoInfoTest.m b/Example/Database/Tests/Unit/FRepoInfoTest.m
new file mode 100644
index 0000000..94e6a70
--- /dev/null
+++ b/Example/Database/Tests/Unit/FRepoInfoTest.m
@@ -0,0 +1,44 @@
+/*
+ * 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 "FRepoInfo.h"
+#import "FTestHelpers.h"
+@interface FRepoInfoTest : XCTestCase
+
+@end
+
+@implementation FRepoInfoTest
+
+- (void) testGetConnectionUrl {
+ FRepoInfo *info = [[FRepoInfo alloc] initWithHost:@"test-namespace.example.com"
+ isSecure:NO
+ withNamespace:@"tests"];
+ XCTAssertEqualObjects(info.connectionURL, @"ws://test-namespace.example.com/.ws?v=5&ns=tests",
+ @"getConnection works");
+}
+
+- (void) testGetConnectionUrlWithLastSession {
+ FRepoInfo *info = [[FRepoInfo alloc] initWithHost:@"tests-namespace.example.com"
+ isSecure:NO
+ withNamespace:@"tests"];
+ XCTAssertEqualObjects([info connectionURLWithLastSessionID:@"testsession"],
+ @"ws://tests-namespace.example.com/.ws?v=5&ns=tests&ls=testsession",
+ @"getConnectionWithLastSession works");
+}
+@end
diff --git a/Example/Database/Tests/Unit/FSparseSnapshotTests.h b/Example/Database/Tests/Unit/FSparseSnapshotTests.h
new file mode 100644
index 0000000..1f0acb2
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSparseSnapshotTests.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FSparseSnapshotTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FSparseSnapshotTests.m b/Example/Database/Tests/Unit/FSparseSnapshotTests.m
new file mode 100644
index 0000000..ab22c0d
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSparseSnapshotTests.m
@@ -0,0 +1,207 @@
+/*
+ * 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 "FSparseSnapshotTests.h"
+#import "FSparseSnapshotTree.h"
+#import "FSnapshotUtilities.h"
+#import "FEmptyNode.h"
+
+@implementation FSparseSnapshotTests
+
+- (void) testBasicRememberAndFind {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ FPath* path = [[FPath alloc] initWith:@"a/b"];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@"sdfsd"];
+
+ [st rememberData:node onPath:path];
+ id<FNode> found = [st findPath:path];
+ XCTAssertFalse([found isEmpty], @"Should find node");
+ found = [st findPath:path.parent];
+ XCTAssertTrue(found == nil, @"Should not find a node");
+}
+
+- (void) testFindInsideAnExistingSnapshot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ FPath* path = [[FPath alloc] initWith:@"t/tt"];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"a": @"sdfsd", @"x": @5, @"999i": @YES}];
+ id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"goats": @88}];
+ node = [node updateImmediateChild:@"apples" withNewChild:update];
+ [st rememberData:node onPath:path];
+
+ id<FNode> found = [st findPath:path];
+ XCTAssertFalse([found isEmpty], @"Should find the node we set");
+ found = [st findPath:[path childFromString:@"a"]];
+ XCTAssertTrue([[found val] isEqualToString:@"sdfsd"], @"Find works inside data snapshot");
+ found = [st findPath:[path childFromString:@"999i"]];
+ XCTAssertTrue([[found val] isEqualToNumber:@YES], @"Find works inside data snapshot");
+ found = [st findPath:[path childFromString:@"apples"]];
+ XCTAssertFalse([found isEmpty], @"Should find the node we set");
+ found = [st findPath:[path childFromString:@"apples/goats"]];
+ XCTAssertTrue([[found val] isEqualToNumber:@88], @"Find works inside data snapshot");
+}
+
+- (void) testWriteASnapshotInsideASnapshot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@19] onPath:[[FPath alloc] initWith:@"t/a/rr"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t/a/b"]];
+ XCTAssertTrue([[found val] isEqualToString:@"v"], @"Find inside snap");
+ found = [st findPath:[[FPath alloc] initWith:@"t/a/rr"]];
+ XCTAssertTrue([[found val] isEqualToNumber:@19], @"Find inside snap");
+}
+
+- (void) testWriteANullValueAndConfirmItIsRemembered {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:[NSNull null]] onPath:[[FPath alloc] initWith:@"awq/fff"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"awq/fff"]];
+ XCTAssertTrue([found isEmpty], @"Empty node");
+ found = [st findPath:[[FPath alloc] initWith:@"awq/sdf"]];
+ XCTAssertTrue(found == nil, @"No node here");
+ found = [st findPath:[[FPath alloc] initWith:@"awq/fff/jjj"]];
+ XCTAssertTrue([found isEmpty], @"Empty node");
+ found = [st findPath:[[FPath alloc] initWith:@"awq/sdf/sdj/q"]];
+ XCTAssertTrue(found == nil, @"No node here");
+}
+
+- (void) testOverwriteWithNullAndConfirmItIsRemembered {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ [st rememberData:[FSnapshotUtilities nodeFrom:[NSNull null]] onPath:[[FPath alloc] initWith:@"t"]];
+ found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertTrue([found isEmpty], @"Empty node");
+}
+
+- (void) testSimpleRememberAndForget {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ [st forgetPath:[[FPath alloc] initWith:@"t"]];
+ found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertTrue(found == nil, @"node is gone");
+}
+
+- (void) testForgetTheRoot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ found = [st findPath:[[FPath alloc] initWith:@""]];
+ XCTAssertTrue(found == nil, @"node is gone");
+}
+
+- (void) testForgetSnapshotInsideSnapshot {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v", @"c": @9, @"art": @NO}}] onPath:[[FPath alloc] initWith:@"t"]];
+ id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t/a/c"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ found = [st findPath:[[FPath alloc] initWith:@"t"]];
+ XCTAssertFalse([found isEmpty], @"non-empty node");
+ [st forgetPath:PATH(@"t/a/c")];
+ XCTAssertTrue([st findPath:PATH(@"t")] == nil, @"no more node here");
+ XCTAssertTrue([st findPath:PATH(@"t/a")] == nil, @"no more node here");
+ XCTAssertTrue([[[st findPath:PATH(@"t/a/b")] val] isEqualToString:@"v"], @"child still exists");
+ XCTAssertTrue([st findPath:PATH(@"t/a/c")] == nil, @"no more node here");
+ XCTAssertTrue([[[st findPath:PATH(@"t/a/art")] val] isEqualToNumber:@NO], @"child still exists");
+}
+
+- (void) testPathShallowerThanSnapshots {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:NODE(@NO) onPath:PATH(@"t/x1")];
+ [st rememberData:NODE(@YES) onPath:PATH(@"t/x2")];
+
+ [st forgetPath:PATH(@"t")];
+ XCTAssertTrue([st findPath:PATH(@"t")] == nil, @"No more node here");
+}
+
+- (void) testIterateChildren {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"b": @"v", @"c": @9, @"art": @NO}];
+ [st rememberData:node onPath:PATH(@"t")];
+ [st rememberData:[FEmptyNode emptyNode] onPath:PATH(@"q")];
+
+ __block int num = 0;
+ __block BOOL gotT = NO;
+ __block BOOL gotQ = NO;
+ [st forEachChild:^(NSString* key, FSparseSnapshotTree* tree) {
+ num++;
+ if ([key isEqualToString:@"t"]) {
+ gotT = YES;
+ } else if ([key isEqualToString:@"q"]) {
+ gotQ = YES;
+ } else {
+ XCTFail(@"Unknown child");
+ }
+ }];
+
+ XCTAssertTrue(gotT, @"Saw t");
+ XCTAssertTrue(gotQ, @"Saw q");
+ XCTAssertTrue(num == 2, @"Saw two children");
+}
+
+- (void) testIterateTrees {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ __block int count = 0;
+ [st forEachTreeAtPath:PATH(@"") do:^(FPath *path, id<FNode> data) {
+ count++;
+ }];
+ XCTAssertTrue(count == 0, @"No trees to iterate through");
+
+ [st rememberData:NODE(@1) onPath:PATH(@"t")];
+ [st rememberData:NODE(@2) onPath:PATH(@"a/b")];
+ [st rememberData:NODE(@3) onPath:PATH(@"a/x/g")];
+ [st rememberData:NODE([NSNull null]) onPath:PATH(@"a/x/null")];
+
+ __block int num = 0;
+ __block BOOL got1 = NO;
+ __block BOOL got2 = NO;
+ __block BOOL got3 = NO;
+ __block BOOL gotNull = NO;
+
+ [st forEachTreeAtPath:PATH(@"q") do:^(FPath *path, id<FNode> data) {
+ num++;
+ NSString* pathString = [path description];
+ if ([pathString isEqualToString:@"/q/t"]) {
+ got1 = YES;
+ XCTAssertTrue([[data val] isEqualToNumber:@1], @"got 1");
+ } else if ([pathString isEqualToString:@"/q/a/b"]) {
+ got2 = YES;
+ XCTAssertTrue([[data val] isEqualToNumber:@2], @"got 2");
+ } else if ([pathString isEqualToString:@"/q/a/x/g"]) {
+ got3 = YES;
+ XCTAssertTrue([[data val] isEqualToNumber:@3], @"got 3");
+ } else if ([pathString isEqualToString:@"/q/a/x/null"]) {
+ gotNull = YES;
+ XCTAssertTrue([data val] == [NSNull null], @"got null");
+ } else {
+ XCTFail(@"unknown tree");
+ }
+ }];
+
+ XCTAssertTrue(got1 && got2 && got3 && gotNull, @"saw all the children");
+ XCTAssertTrue(num == 4, @"Saw the right number of children");
+}
+
+- (void) testSetLeafAndForgetDeeperPath {
+ FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init];
+ [st rememberData:NODE(@"bar") onPath:PATH(@"foo")];
+ BOOL safeToRemove = [st forgetPath:PATH(@"foo/baz")];
+ XCTAssertFalse(safeToRemove, @"Should not have deleted anything, nothing to remove");
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FSyncPointTests.h b/Example/Database/Tests/Unit/FSyncPointTests.h
new file mode 100644
index 0000000..bc010ae
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSyncPointTests.h
@@ -0,0 +1,21 @@
+/*
+ * 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 "FTestBase.h"
+
+@interface FSyncPointTests : FTestBase
+
+@end
diff --git a/Example/Database/Tests/Unit/FSyncPointTests.m b/Example/Database/Tests/Unit/FSyncPointTests.m
new file mode 100644
index 0000000..d36b48a
--- /dev/null
+++ b/Example/Database/Tests/Unit/FSyncPointTests.m
@@ -0,0 +1,905 @@
+/*
+ * 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 "FSyncPointTests.h"
+#import "FListenProvider.h"
+#import "FQuerySpec.h"
+#import "FQueryParams.h"
+#import "FPathIndex.h"
+#import "FKeyIndex.h"
+#import "FPriorityIndex.h"
+#import "FIRDatabaseQuery_Private.h"
+#import "FSyncTree.h"
+#import "FChange.h"
+#import "FDataEvent.h"
+#import "FIRDataSnapshot_Private.h"
+#import "FCancelEvent.h"
+#import "FSnapshotUtilities.h"
+#import "FEventRegistration.h"
+#import "FCompoundWrite.h"
+#import "FEmptyNode.h"
+#import "FTestClock.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FSnapshotUtilities.h"
+
+typedef NSDictionary* (^fbt_nsdictionary_void)(void);
+
+@interface FTestEventRegistration : NSObject<FEventRegistration>
+@property (nonatomic, strong) NSDictionary *spec;
+@property (nonatomic, strong) FQuerySpec *query;
+@end
+
+@implementation FTestEventRegistration
+- (id) initWithSpec:(NSDictionary *)eventSpec query:(FQuerySpec *)query {
+ self = [super init];
+ if (self) {
+ self.spec = eventSpec;
+ self.query = query;
+ }
+ return self;
+}
+
+- (BOOL) responseTo:(FIRDataEventType)eventType {
+ return YES;
+}
+- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query {
+ FIRDataSnapshot *snap = nil;
+ FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:nil path:query.path];
+ if (change.type == FIRDataEventTypeValue) {
+ snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode];
+ } else {
+ snap = [[FIRDataSnapshot alloc] initWithRef:[ref child:change.childKey]
+ indexedNode:change.indexedNode];
+ }
+ return [[FDataEvent alloc] initWithEventType:change.type eventRegistration:self dataSnapshot:snap prevName:change.prevKey];
+}
+
+- (BOOL) matches:(id<FEventRegistration>)other {
+ if (![other isKindOfClass:[FTestEventRegistration class]]) {
+ return NO;
+ } else {
+ FTestEventRegistration *otherRegistration = other;
+ if (self.spec[@"callbackId"] && otherRegistration.spec[@"callbackId"] &&
+ [self.spec[@"callbackId"] isEqualToNumber:otherRegistration.spec[@"callbackId"]]) {
+ return YES;
+ } else {
+ return NO;
+ }
+ }
+}
+
+- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue {
+ [NSException raise:@"NotImplementedError" format:@"Method not implemneted."];
+}
+- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path {
+ [NSException raise:@"NotImplementedError" format:@"Method not implemneted."];
+ return nil;
+}
+
+- (FIRDatabaseHandle) handle {
+ [NSException raise:@"NotImplementedError" format:@"Method not implemneted."];
+ return 0;
+}
+@end
+
+@implementation FSyncPointTests
+
+- (NSString *) queryKeyForQuery:(FQuerySpec *)query tagId:(NSNumber *)tagId {
+ return [NSString stringWithFormat:@"%@|%@|%@", query.path, query.params, tagId];
+}
+
+- (void) actualEvent:(FDataEvent *)actual equalsExpected:(NSDictionary *)expected {
+ XCTAssertEqual(actual.eventType, [self stringToEventType:expected[@"type"]], @"Event type should be equal");
+ if (actual.eventType != FIRDataEventTypeValue) {
+ NSString *childName = actual.snapshot.key;
+ XCTAssertEqualObjects(childName, expected[@"name"], @"Snapshot name should be equal");
+ if (expected[@"prevName"] == [NSNull null]) {
+ XCTAssertNil(actual.prevName, @"prevName should be nil");
+ } else {
+ XCTAssertEqualObjects(actual.prevName, expected[@"prevName"], @"prevName should be equal");
+ }
+ }
+ NSString *actualHash = [actual.snapshot.node.node dataHash];
+ NSString *expectedHash = [[FSnapshotUtilities nodeFrom:expected[@"data"]] dataHash];
+ XCTAssertEqualObjects(actualHash, expectedHash, @"Data hash should be equal");
+}
+
+/**
+* @param actual is an array of id<FEvent>
+* @param expected is an array of dictionaries?
+*/
+- (void) actualEvents:(NSArray *)actual exactMatchesExpected:(NSArray *)expected {
+ if ([expected count] < [actual count]) {
+ XCTFail(@"Got extra events: %@", actual);
+ } else if ([expected count] > [actual count]) {
+ XCTFail(@"Missing events: %@", actual);
+ } else {
+ NSUInteger i = 0;
+ for (i = 0; i < [expected count]; i++) {
+ FDataEvent *actualEvent = actual[i];
+ NSDictionary *expectedEvent = expected[i];
+ [self actualEvent:actualEvent equalsExpected:expectedEvent];
+ }
+ }
+}
+
+- (void)assertOrderedFirstEvent:(FIRDataEventType)e1 secondEvent:(FIRDataEventType)e2 {
+ static NSArray *eventOrdering = nil;
+ if (!eventOrdering) {
+ eventOrdering = @[
+ [NSNumber numberWithInteger:FIRDataEventTypeChildRemoved],
+ [NSNumber numberWithInteger:FIRDataEventTypeChildAdded],
+ [NSNumber numberWithInteger:FIRDataEventTypeChildMoved],
+ [NSNumber numberWithInteger:FIRDataEventTypeChildChanged],
+ [NSNumber numberWithInteger:FIRDataEventTypeValue]
+ ];
+ }
+ NSUInteger idx1 = [eventOrdering indexOfObject:[NSNumber numberWithInteger:e1]];
+ NSUInteger idx2 = [eventOrdering indexOfObject:[NSNumber numberWithInteger:e2]];
+ if (idx1 > idx2) {
+ XCTFail(@"Received %d after %d", (int)e2, (int)e1);
+ }
+}
+
+- (FIRDataEventType)stringToEventType:(NSString *)stringType {
+ if ([stringType isEqualToString:@"child_added"]) {
+ return FIRDataEventTypeChildAdded;
+ } else if ([stringType isEqualToString:@"child_removed"]) {
+ return FIRDataEventTypeChildRemoved;
+ } else if ([stringType isEqualToString:@"child_changed"]) {
+ return FIRDataEventTypeChildChanged;
+ } else if ([stringType isEqualToString:@"child_moved"]) {
+ return FIRDataEventTypeChildMoved;
+ } else if ([stringType isEqualToString:@"value"]) {
+ return FIRDataEventTypeValue;
+ } else {
+ XCTFail(@"Unknown event type %@", stringType);
+ return FIRDataEventTypeValue;
+ }
+}
+
+- (void) actualEventSet:(id)actual matchesExpected:(id)expected atBasePath:(NSString *)basePathStr {
+ // don't worry about order for now
+ XCTAssertEqual([expected count], [actual count], @"Mismatched lengths.\nExpected: %@\nActual: %@", expected, actual);
+
+ NSArray *currentExpected = expected;
+ NSArray *currentActual = actual;
+ FPath *basePath = basePathStr != nil ? [[FPath alloc] initWith:basePathStr] : [FPath empty];
+ while ([currentExpected count] > 0) {
+ // Step 1: find location range in expected
+ // we expect all events for a particular path to be in a group
+ FPath *currentPath = [basePath childFromString:currentExpected[0][@"path"]];
+ NSUInteger i = 1;
+ while (i < [currentExpected count]) {
+ FPath *otherPath = [basePath childFromString:currentExpected[i][@"path"]];
+ if ([currentPath isEqual:otherPath]) {
+ i++;
+ } else {
+ break;
+ }
+ }
+
+ // Step 2: foreach in actual, asserting location
+ NSUInteger j = 0;
+ for (j = 0; j < i; j++) {
+ FDataEvent *actualEventData = currentActual[j];
+ FTestEventRegistration *eventRegistration = actualEventData.eventRegistration;
+ NSDictionary *specStep = eventRegistration.spec;
+ FPath *actualPath = [basePath childFromString:specStep[@"path"]];
+ if (![currentPath isEqual:actualPath]) {
+ XCTFail(@"Expected path %@ to equal %@", actualPath, currentPath);
+ }
+ }
+
+ // Step 3: slice each array
+ NSMutableArray *expectedSlice = [[currentExpected subarrayWithRange:NSMakeRange(0, i)] mutableCopy];
+ NSArray *actualSlice = [currentActual subarrayWithRange:NSMakeRange(0, i)];
+
+ // foreach in actual, stack up to enforce ordering, find in expected
+ NSMutableDictionary *actualMap = [[NSMutableDictionary alloc] init];
+ for (FDataEvent *actualEvent in actualSlice) {
+ FTestEventRegistration *eventRegistration = actualEvent.eventRegistration;
+ FQuerySpec *query = eventRegistration.query;
+ NSDictionary *spec = eventRegistration.spec;
+ NSString *listenId = [NSString stringWithFormat:@"%@|%@", [basePath childFromString:spec[@"path"]], query];
+ if (actualMap[listenId]) {
+ // stack this event up, and make sure it obeys ordering constraints
+ NSMutableArray *eventStack = actualMap[listenId];
+ FDataEvent *prevEvent = eventStack[[eventStack count] - 1];
+ [self assertOrderedFirstEvent:prevEvent.eventType secondEvent:actualEvent.eventType];
+ [eventStack addObject:actualEvent];
+ } else {
+ // this is the first event for this listen, just initialize it
+ actualMap[listenId] = [[NSMutableArray alloc] initWithObjects:actualEvent, nil];
+ }
+ // Ordering has been enforced, make sure we can find this in the expected events
+ __block NSUInteger indexToRemove = NSNotFound;
+ [expectedSlice enumerateObjectsUsingBlock:^(NSDictionary *expectedEvent, NSUInteger idx, BOOL *stop) {
+ if ([self stringToEventType:expectedEvent[@"type"]] == actualEvent.eventType) {
+ if ([self stringToEventType:expectedEvent[@"type"]] != FIRDataEventTypeValue) {
+ if (![expectedEvent[@"name"] isEqualToString:actualEvent.snapshot.key]) {
+ return; // short circuit, not a match
+ }
+ if ([self stringToEventType:expectedEvent[@"type"]] != FIRDataEventTypeChildRemoved &&
+ !(expectedEvent[@"prevName"] == [NSNull null] && actualEvent.prevName == nil) &&
+ !(expectedEvent[@"prevName"] != [NSNull null] && [expectedEvent[@"prevName"] isEqualToString:actualEvent.prevName])) {
+ return; // short circuit, not a match
+ }
+ }
+ // make sure the snapshots match
+ NSString *snapHash = [actualEvent.snapshot.node.node dataHash];
+ NSString *expectedHash = [[FSnapshotUtilities nodeFrom:expectedEvent[@"data"]] dataHash];
+ if ([snapHash isEqualToString:expectedHash]) {
+ indexToRemove = idx;
+ *stop = YES;
+ }
+ }
+ }];
+ XCTAssertFalse(indexToRemove == NSNotFound, @"Could not find matching expected event for %@", actualEvent);
+ [expectedSlice removeObjectAtIndex:indexToRemove];
+ }
+ currentExpected = [currentExpected subarrayWithRange:NSMakeRange(i, [currentExpected count] - i)];
+ currentActual = [currentActual subarrayWithRange:NSMakeRange(i, [currentActual count] - i)];
+ }
+}
+
+- (FQuerySpec *)parseParams:(NSDictionary *)specParams forPath:(FPath *)path {
+ FQueryParams *query = [[FQueryParams alloc] init];
+ NSMutableDictionary *params;
+
+ if (specParams) {
+ params = [specParams mutableCopy];
+ if (!params[@"tag"]) {
+ XCTFail(@"Error: Non-default queries must have tag");
+ }
+ } else {
+ params = [NSMutableDictionary dictionary];
+ }
+
+ if (params[@"orderBy"]) {
+ FPath *indexPath = [FPath pathWithString:params[@"orderBy"]];
+ id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath];
+ query = [query orderBy:index];
+ [params removeObjectForKey:@"orderBy"];
+ }
+ if (params[@"orderByKey"]) {
+ query = [query orderBy:[FKeyIndex keyIndex]];
+ [params removeObjectForKey:@"orderByKey"];
+ }
+ if (params[@"orderByPriority"]) {
+ query = [query orderBy:[FPriorityIndex priorityIndex]];
+ [params removeObjectForKey:@"orderByPriority"];
+ }
+
+ if (params[@"startAt"]) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"startAt"][@"index"]];
+ if (params[@"startAt"][@"name"]) {
+ query = [query startAt:node childKey:params[@"startAt"][@"name"]];
+ } else {
+ query = [query startAt:node];
+ }
+ [params removeObjectForKey:@"startAt"];
+ }
+ if (params[@"endAt"]) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"endAt"][@"index"]];
+ if (params[@"endAt"][@"name"]) {
+ query = [query endAt:node childKey:params[@"endAt"][@"name"]];
+ } else {
+ query = [query endAt:node];
+ }
+ [params removeObjectForKey:@"endAt"];
+ }
+ if (params[@"equalTo"]) {
+ id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"equalTo"][@"index"]];
+ if (params[@"equalTo"][@"name"]) {
+ NSString *name = params[@"equalTo"][@"name"];
+ query = [[query startAt:node childKey:name] endAt:node childKey:name];
+ } else {
+ query = [[query startAt:node] endAt:node];
+ }
+ [params removeObjectForKey:@"equalTo"];
+ }
+
+ if (params[@"limitToFirst"]) {
+ query = [query limitToFirst:[params[@"limitToFirst"] integerValue]];
+ [params removeObjectForKey:@"limitToFirst"];
+ }
+ if (params[@"limitToLast"]) {
+ query = [query limitToLast:[params[@"limitToLast"] integerValue]];
+ [params removeObjectForKey:@"limitToLast"];
+ }
+
+ [params removeObjectForKey:@"tag"];
+ if ([params count] > 0) {
+ XCTFail(@"Unsupported query parameter: %@", params);
+ }
+ return [[FQuerySpec alloc] initWithPath:path params:query];
+}
+
+- (void) runTest:(NSDictionary *)testSpec atBasePath:(NSString *)basePath {
+ NSMutableDictionary *listens = [[NSMutableDictionary alloc] init];
+ __weak FSyncPointTests *weakSelf = self;
+
+ FListenProvider *listenProvider = [[FListenProvider alloc] init];
+ listenProvider.startListening = ^(FQuerySpec *query, NSNumber *tagId, id<FSyncTreeHash> hash, fbt_nsarray_nsstring onComplete) {
+ FQueryParams *queryParams = query.params;
+ FPath *path = query.path;
+ NSString *logTag = [NSString stringWithFormat:@"%@ (%@)", queryParams, tagId];
+ NSString *key = [weakSelf queryKeyForQuery:query tagId:tagId];
+ FFLog(@"I-RDB143001", @"Listening at %@ for %@", path, logTag);
+ id existing = listens[key];
+ NSAssert(existing == nil, @"Duplicate listen");
+ listens[key] = @YES;
+ return @[];
+ };
+
+ listenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tagId) {
+ FQueryParams *queryParams = query.params;
+ FPath *path = query.path;
+ NSString *logTag = [NSString stringWithFormat:@"%@ (%@)", queryParams, tagId];
+ NSString *key = [weakSelf queryKeyForQuery:query tagId:tagId];
+ FFLog(@"I-RDB143002", @"Stop listening at %@ for %@", path, logTag);
+ id existing = listens[key];
+ XCTAssertTrue(existing != nil, @"Missing record of query that we're removing");
+ [listens removeObjectForKey:key];
+ };
+
+ FSyncTree *syncTree = [[FSyncTree alloc] initWithListenProvider:listenProvider];
+
+ NSLog(@"Running %@", testSpec[@"name"]);
+ NSInteger currentWriteId = 0;
+ for (NSDictionary *step in testSpec[@"steps"]) {
+ NSMutableDictionary *spec = [step mutableCopy];
+ if (spec[@".comment"]) {
+ NSLog(@" > %@", spec[@".comment"]);
+ }
+ if (spec[@"debug"] != nil) {
+ // TODO: Ideally we'd pause the debugger somehow (like "debugger;" in JS).
+ NSLog(@"Start debugging");
+ }
+ // Almost everything has a path...
+ FPath *path = [FPath empty];
+ if (basePath != nil) {
+ path = [path childFromString:basePath];
+ }
+ if (spec[@"path"] != nil) {
+ path = [path childFromString:spec[@"path"]];
+ }
+ NSArray *events;
+ if ([spec[@"type"] isEqualToString:@"listen"]) {
+ FQuerySpec *query = [self parseParams:spec[@"params"] forPath:path];
+ FTestEventRegistration *eventRegistration = [[FTestEventRegistration alloc] initWithSpec:spec query:query];
+ events = [syncTree addEventRegistration:eventRegistration forQuery:query];
+ [self actualEvents:events exactMatchesExpected:spec[@"events"]];
+
+ } else if ([spec[@"type"] isEqualToString:@"unlisten"]) {
+ FQuerySpec *query = [self parseParams:spec[@"params"] forPath:path];
+ FTestEventRegistration *eventRegistration = [[FTestEventRegistration alloc] initWithSpec:spec query:query];
+ events = [syncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil];
+ [self actualEvents:events exactMatchesExpected:spec[@"events"]];
+
+ } else if ([spec[@"type"] isEqualToString:@"serverUpdate"]) {
+ id<FNode> update = [FSnapshotUtilities nodeFrom:spec[@"data"]];
+ if (spec[@"tag"]) {
+ events = [syncTree applyTaggedQueryOverwriteAtPath:path newData:update tagId:spec[@"tag"]];
+ } else {
+ events = [syncTree applyServerOverwriteAtPath:path newData:update];
+ }
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+
+ } else if ([spec[@"type"] isEqualToString:@"serverMerge"]) {
+ FCompoundWrite *compoundWrite = [FCompoundWrite compoundWriteWithValueDictionary:spec[@"data"]];
+ if (spec[@"tag"]) {
+ events = [syncTree applyTaggedQueryMergeAtPath:path changedChildren:compoundWrite tagId:spec[@"tag"]];
+ } else {
+ events = [syncTree applyServerMergeAtPath:path changedChildren:compoundWrite];
+ }
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+
+ } else if ([spec[@"type"] isEqualToString:@"set"]) {
+ id<FNode> toSet = [FSnapshotUtilities nodeFrom:spec[@"data"]];
+ BOOL visible = (spec[@"visible"] != nil) ? [spec[@"visible"] boolValue] : YES;
+ events = [syncTree applyUserOverwriteAtPath:path newData:toSet writeId:currentWriteId++ isVisible:visible];
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+
+ } else if ([spec[@"type"] isEqualToString:@"update"]) {
+ FCompoundWrite *compoundWrite = [FCompoundWrite compoundWriteWithValueDictionary:spec[@"data"]];
+ events = [syncTree applyUserMergeAtPath:path changedChildren:compoundWrite writeId:currentWriteId++];
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+ } else if ([spec[@"type"] isEqualToString:@"ackUserWrite"]) {
+ NSInteger writeId = [spec[@"writeId"] integerValue];
+ BOOL revert = [spec[@"revert"] boolValue];
+ events = [syncTree ackUserWriteWithWriteId:writeId revert:revert persist:YES clock:[[FTestClock alloc] init]];
+ [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath];
+ } else if ([spec[@"type"] isEqualToString:@"suppressWarning"]) {
+ // Do nothing. This is a hack so JS's Jasmine tests don't throw warnings for "expect no errors" tests.
+ } else {
+ XCTFail(@"Unknown step: %@", spec[@"type"]);
+ }
+ }
+}
+
+- (NSArray *) loadSpecs {
+ static NSArray *json;
+ if (json == nil) {
+ NSString *syncPointSpec = [[NSBundle bundleForClass:[FSyncPointTests class]] pathForResource:@"syncPointSpec" ofType:@"json"];
+ NSLog(@"%@", syncPointSpec);
+ NSData *specData = [NSData dataWithContentsOfFile:syncPointSpec];
+ NSError *error = nil;
+ json = [NSJSONSerialization JSONObjectWithData:specData options:kNilOptions error:&error];
+
+ if (error) {
+ XCTFail(@"Error occurred parsing JSON: %@", error);
+ }
+ }
+
+ return json;
+}
+
+- (NSDictionary *) specsForName:(NSString *)name {
+ for (NSDictionary *spec in [self loadSpecs]) {
+ if ([name isEqualToString:spec[@"name"]]) {
+ return spec;
+ }
+ }
+
+ XCTFail(@"No such test: %@", name);
+ return nil;
+}
+
+- (void) runTestForName:(NSString *)name {
+ NSDictionary *spec = [self specsForName:name];
+ [self runTest:spec atBasePath:nil];
+ // run again at a deeper location
+ [self runTest:spec atBasePath:@"/foo/bar/baz"];
+}
+
+- (void) testAll {
+ NSArray *specs = [self loadSpecs];
+ for (NSDictionary *spec in specs) {
+ [self runTest:spec atBasePath:nil];
+ // run again at a deeper location
+ [self runTest:spec atBasePath:@"/foo/bar/baz"];
+ }
+}
+
+- (void) testDefaultListenHandlesParentSet {
+ [self runTestForName:@"Default listen handles a parent set"];
+}
+
+- (void) testDefaultListenHandlesASetAtTheSameLevel {
+ [self runTestForName:@"Default listen handles a set at the same level"];
+}
+
+- (void) testAQueryCanGetACompleteCacheThenAMerge {
+ [self runTestForName:@"A query can get a complete cache then a merge"];
+}
+
+- (void) testServerMergeOnListenerWithCompleteChildren {
+ [self runTestForName:@"Server merge on listener with complete children"];
+}
+
+- (void) testDeepMergeOnListenerWithCompleteChildren {
+ [self runTestForName:@"Deep merge on listener with complete children"];
+}
+
+- (void) testUpdateChildListenerTwice {
+ [self runTestForName:@"Update child listener twice"];
+}
+
+- (void) testChildOfDefaultListenThatAlreadyHasACompleteCache {
+ [self runTestForName:@"Update child of default listen that already has a complete cache"];
+}
+
+- (void) testUpdateChildOfDefaultListenThatHasNoCache {
+ [self runTestForName:@"Update child of default listen that has no cache"];
+}
+
+// failing
+- (void) testUpdateTheChildOfACoLocatedDefaultListenerAndQuery {
+ [self runTestForName:@"Update (via set) the child of a co-located default listener and query"];
+}
+
+- (void) testUpdateTheChildOfAQueryWithAFullCache {
+ [self runTestForName:@"Update (via set) the child of a query with a full cache"];
+}
+
+- (void) testUpdateAChildBelowAnEmptyQuery {
+ [self runTestForName:@"Update (via set) a child below an empty query"];
+}
+
+- (void) testUpdateDescendantOfDefaultListenerWithFullCache {
+ [self runTestForName:@"Update descendant of default listener with full cache"];
+}
+
+- (void) testDescendantSetBelowAnEmptyDefaultLIstenerIsIgnored {
+ [self runTestForName:@"Descendant set below an empty default listener is ignored"];
+}
+
+- (void) testUpdateOfAChild {
+ [self runTestForName:@"Update of a child. This can happen if a child listener is added and removed"];
+}
+
+- (void) testRevertSetWithOnlyChildCaches {
+ [self runTestForName:@"Revert set with only child caches"];
+}
+
+- (void) testCanRevertADuplicateChildSet {
+ [self runTestForName:@"Can revert a duplicate child set"];
+}
+
+- (void) testCanRevertAChildSetAndSeeTheUnderlyingData {
+ [self runTestForName:@"Can revert a child set and see the underlying data"];
+}
+
+- (void) testRevertChildSetWithNoServerData {
+ [self runTestForName:@"Revert child set with no server data"];
+}
+
+- (void) testRevertDeepSetWithNoServerData {
+ [self runTestForName:@"Revert deep set with no server data"];
+}
+
+- (void) testRevertSetCoveredByNonvisibleTransaction {
+ [self runTestForName:@"Revert set covered by non-visible transaction"];
+}
+
+- (void) testClearParentShadowingServerValuesSetWithServerChildren {
+ [self runTestForName:@"Clear parent shadowing server values set with server children"];
+}
+
+- (void) testClearChildShadowingServerValuesSetWithServerChildren {
+ [self runTestForName:@"Clear child shadowing server values set with server children"];
+}
+
+- (void) testUnrelatedMergeDoesntShadowServerUpdates {
+ [self runTestForName:@"Unrelated merge doesn't shadow server updates"];
+}
+
+- (void) testCanSetAlongsideARemoteMerge {
+ [self runTestForName:@"Can set alongside a remote merge"];
+}
+
+- (void) testSetPriorityOnALocationWithNoCache {
+ [self runTestForName:@"setPriority on a location with no cache"];
+}
+
+- (void) testDeepUpdateDeletesChildFromLimitWindowAndPullsInNewChild {
+ [self runTestForName:@"deep update deletes child from limit window and pulls in new child"];
+}
+
+- (void) testDeepSetDeletesChildFromLimitWindowAndPullsInNewChild {
+ [self runTestForName:@"deep set deletes child from limit window and pulls in new child"];
+}
+
+- (void) testEdgeCaseInNewChildForChange {
+ [self runTestForName:@"Edge case in newChildForChange_"];
+}
+
+- (void) testRevertSetInQueryWindow {
+ [self runTestForName:@"Revert set in query window"];
+}
+
+- (void) testHandlesAServerValueMovingAChildOutOfAQueryWindow {
+ [self runTestForName:@"Handles a server value moving a child out of a query window"];
+}
+
+- (void) testUpdateOfIndexedChildWorks {
+ [self runTestForName:@"Update of indexed child works"];
+}
+
+- (void) testMergeAppliedToEmptyLimit {
+ [self runTestForName:@"Merge applied to empty limit"];
+}
+
+- (void) testLimitIsRefilledFromServerDataAfterMerge {
+ [self runTestForName:@"Limit is refilled from server data after merge"];
+}
+
+- (void) testHandleRepeatedListenWithMergeAsFirstUpdate {
+ [self runTestForName:@"Handle repeated listen with merge as first update"];
+}
+
+- (void) testLimitIsRefilledFromServerDataAfterSet {
+ [self runTestForName:@"Limit is refilled from server data after set"];
+}
+
+- (void) testQueryOnWeirdPath {
+ [self runTestForName:@"query on weird path."];
+}
+
+- (void) testRunsRound2 {
+ [self runTestForName:@"runs, round2"];
+}
+
+- (void) testHandlesNestedListens {
+ [self runTestForName:@"handles nested listens"];
+}
+
+- (void) testHandlesASetBelowAListen {
+ [self runTestForName:@"Handles a set below a listen"];
+}
+
+- (void) testDoesNonDefaultQueries {
+ [self runTestForName:@"does non-default queries"];
+}
+
+- (void) testHandlesCoLocatedDefaultListenerAndQuery {
+ [self runTestForName:@"handles a co-located default listener and query"];
+}
+
+- (void) testDefaultAndNonDefaultListenerAtSameLocationWithServerUpdate {
+ [self runTestForName:@"Default and non-default listener at same location with server update"];
+}
+
+- (void) testAddAParentListenerToACompleteChildListenerExpectChildEvent {
+ [self runTestForName:@"Add a parent listener to a complete child listener, expect child event"];
+}
+
+- (void) testAddListensToASetExpectCorrectEventsIncludingAChildEvent {
+ [self runTestForName:@"Add listens to a set, expect correct events, including a child event"];
+}
+
+- (void) testServerUpdateToAChildListenerRaisesChildEventsAtParent {
+ [self runTestForName:@"ServerUpdate to a child listener raises child events at parent"];
+}
+
+- (void) testServerUpdateToAChildListenerRaisesChildEventsAtParentQuery {
+ [self runTestForName:@"ServerUpdate to a child listener raises child events at parent query"];
+}
+
+- (void) testMultipleCompleteChildrenAreHandleProperly {
+ [self runTestForName:@"Multiple complete children are handled properly"];
+}
+
+- (void) testWriteLeafNodeOverwriteAtParentNode {
+ [self runTestForName:@"Write leaf node, overwrite at parent node"];
+}
+
+- (void) testConfirmCompleteChildrenFromTheServer {
+ [self runTestForName:@"Confirm complete children from the server"];
+}
+
+- (void) testWriteLeafOverwriteFromParent {
+ [self runTestForName:@"Write leaf, overwrite from parent"];
+}
+
+- (void) testBasicUpdateTest {
+ [self runTestForName:@"Basic update test"];
+}
+
+- (void) testNoDoubleValueEventsForUserAck {
+ [self runTestForName:@"No double value events for user ack"];
+}
+
+- (void) testBasicKeyIndexSanityCheck {
+ [self runTestForName:@"Basic key index sanity check"];
+}
+
+- (void) testCollectCorrectSubviewsToListenOn {
+ [self runTestForName:@"Collect correct subviews to listen on"];
+}
+
+- (void) testLimitToFirstOneOnOrderedQuery {
+ [self runTestForName:@"Limit to first one on ordered query"];
+}
+
+- (void) testLimitToLastOneOnOrderedQuery {
+ [self runTestForName:@"Limit to last one on ordered query"];
+}
+
+- (void) testUpdateIndexedValueOnExistingChildFromLimitedQuery {
+ [self runTestForName:@"Update indexed value on existing child from limited query"];
+}
+
+- (void) testCanCreateStartAtEndAtEqualToQueriesWithBool {
+ [self runTestForName:@"Can create startAt, endAt, equalTo queries with bool"];
+}
+
+- (void) testQueryWithExistingServerSnap {
+ [self runTestForName:@"Query with existing server snap"];
+}
+
+- (void) testServerDataIsNotPurgedForNonServerIndexedQueries {
+ [self runTestForName:@"Server data is not purged for non-server-indexed queries"];
+}
+
+- (void) testStartAtEndAtDominatesLimit {
+ [self runTestForName:@"startAt/endAt dominates limit"];
+}
+
+- (void) testUpdateToSingleChildThatMovesOutOfWindow {
+ [self runTestForName:@"Update to single child that moves out of window"];
+}
+
+- (void) testLimitedQueryDoesntPullInOutOfRangeChild {
+ [self runTestForName:@"Limited query doesn't pull in out of range child"];
+}
+
+- (void) testWithCustomOrderByIsRefilledWithCorrectItem {
+ [self runTestForName:@"Limit with custom orderBy is refilled with correct item"];
+}
+
+- (void) testMergeForLocationWithDefaultAndLimitedListener {
+ [self runTestForName:@"Merge for location with default and limited listener"];
+}
+
+- (void) testUserMergePullsInCorrectValues {
+ [self runTestForName:@"User merge pulls in correct values"];
+}
+
+- (void) testUserDeepSetPullsInCorrectValues {
+ [self runTestForName:@"User deep set pulls in correct values"];
+}
+
+- (void) testQueriesWithEqualToNullWork {
+ [self runTestForName:@"Queries with equalTo(null) work"];
+}
+
+- (void) testRevertedWritesUpdateQuery {
+ [self runTestForName:@"Reverted writes update query"];
+}
+
+- (void) testDeepSetForNonLocalDataDoesntRaiseEvents {
+ [self runTestForName:@"Deep set for non-local data doesn't raise events"];
+}
+
+- (void) testUserUpdateWithNewChildrenTriggersEvents {
+ [self runTestForName:@"User update with new children triggers events"];
+}
+
+- (void) testUserWriteWithDeepOverwrite {
+ [self runTestForName:@"User write with deep user overwrite"];
+}
+
+- (void) testServerUpdatesPriority {
+ [self runTestForName:@"Server updates priority"];
+}
+
+- (void) testRevertFullUnderlyingWrite {
+ [self runTestForName:@"Revert underlying full overwrite"];
+}
+
+- (void) testUserChildOverwriteForNonexistentServerNode {
+ [self runTestForName:@"User child overwrite for non-existent server node"];
+}
+
+- (void) testRevertUserOverwriteOfChildOnLeafNode {
+ [self runTestForName:@"Revert user overwrite of child on leaf node"];
+}
+
+- (void) testServerOverwriteWithDeepUserDelete {
+ [self runTestForName:@"Server overwrite with deep user delete"];
+}
+
+- (void) testUserOverwritesLeafNodeWithPriority {
+ [self runTestForName:@"User overwrites leaf node with priority"];
+}
+
+- (void) testUserOverwritesInheritPriorityValuesFromLeafNodes {
+ [self runTestForName:@"User overwrites inherit priority values from leaf nodes"];
+}
+
+- (void) testUserUpdateOnUserSetLeafNodeWithPriorityAfterServerUpdate {
+ [self runTestForName:@"User update on user set leaf node with priority after server update"];
+}
+
+- (void) testServerDeepDeleteOnLeafNode {
+ [self runTestForName:@"Server deep delete on leaf node"];
+}
+
+- (void) testUserSetsRootPriority {
+ [self runTestForName:@"User sets root priority"];
+}
+
+- (void) testUserUpdatesPriorityOnEmptyRoot {
+ [self runTestForName:@"User updates priority on empty root"];
+}
+
+- (void) testRevertSetAtRootWithPriority {
+ [self runTestForName:@"Revert set at root with priority"];
+}
+
+- (void) testServerUpdatesPriorityAfterUserSetsPriority {
+ [self runTestForName:@"Server updates priority after user sets priority"];
+}
+
+- (void) testEmptySetDoesntPreventServerUpdates {
+ [self runTestForName:@"Empty set doesn't prevent server updates"];
+}
+
+- (void) testUserUpdatesPriorityTwiceFirstIsReverted {
+ [self runTestForName:@"User updates priority twice, first is reverted"];
+}
+
+- (void) testServerAcksRootPrioritySetAfterUserDeletesRootNode {
+ [self runTestForName:@"Server acks root priority set after user deletes root node"];
+}
+
+- (void) testADeleteInAMergeDoesntPushOutNodes {
+ [self runTestForName:@"A delete in a merge doesn't push out nodes"];
+}
+
+- (void) testATaggedQueryFiresEventsEventually {
+ [self runTestForName:@"A tagged query fires events eventually"];
+}
+
+- (void) testUserWriteOutsideOfLimitIsIgnoredForTaggedQueries {
+ [self runTestForName:@"User write outside of limit is ignored for tagged queries"];
+}
+
+- (void) testAckForMergeDoesntRaiseValueEventForLaterListen {
+ [self runTestForName:@"Ack for merge doesn't raise value event for later listen"];
+}
+
+- (void) testClearParentShadowingServerValuesMergeWithServerChildren {
+ [self runTestForName:@"Clear parent shadowing server values merge with server children"];
+}
+
+- (void) testPrioritiesDontMakeMeSick {
+ [self runTestForName:@"Priorities don't make me sick"];
+}
+
+- (void) testMergeThatMovesChildFromWindowToBoundaryDoesNotCauseChildToBeReadded {
+ [self runTestForName:@"Merge that moves child from window to boundary does not cause child to be readded"];
+}
+
+- (void) testDeepMergeAckIsHandledCorrectly {
+ [self runTestForName:@"Deep merge ack is handled correctly."];
+}
+
+- (void) testDeepMergeAckOnIncompleteDataAndWithServerValues {
+ [self runTestForName:@"Deep merge ack (on incomplete data, and with server values)"];
+}
+
+- (void) testLimitQueryHandlesDeepServerMergeForOutOfViewItem {
+ [self runTestForName:@"Limit query handles deep server merge for out-of-view item."];
+}
+
+- (void) testLimitQueryHandlesDeepUserMergeForOutOfViewItem {
+ [self runTestForName:@"Limit query handles deep user merge for out-of-view item."];
+}
+
+- (void) testLimitQueryHandlesDeepUserMergeForOutOfViewItemFollowedByServerUpdate {
+ [self runTestForName:@"Limit query handles deep user merge for out-of-view item followed by server update."];
+}
+
+- (void) testUnrelatedUntaggedUpdateIsNotCachedInTaggedListen {
+ [self runTestForName:@"Unrelated, untagged update is not cached in tagged listen"];
+}
+
+- (void) testUnrelatedAckedSetIsNotCachedInTaggedListen {
+ [self runTestForName:@"Unrelated, acked set is not cached in tagged listen"];
+}
+
+- (void) testUnrelatedAckedUpdateIsNotCachedInTaggedListen {
+ [self runTestForName:@"Unrelated, acked update is not cached in tagged listen"];
+}
+
+- (void) testdeepUpdateRaisesImmediateEventsOnlyIfHasCompleteData {
+ [self runTestForName:@"Deep update raises immediate events only if has complete data"];
+}
+
+- (void) testdeepUpdateReturnsMinimumDataRequired {
+ [self runTestForName:@"Deep update returns minimum data required"];
+}
+
+- (void) testdeepUpdateRaisesAllEvents {
+ [self runTestForName:@"Deep update raises all events"];
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m b/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m
new file mode 100644
index 0000000..ebcf9b2
--- /dev/null
+++ b/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m
@@ -0,0 +1,338 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+#import "FTrackedQueryManager.h"
+#import "FTrackedQuery.h"
+#import "FMockStorageEngine.h"
+#import "FPath.h"
+#import "FQuerySpec.h"
+#import "FPathIndex.h"
+#import "FSnapshotUtilities.h"
+#import "FClock.h"
+#import "FTestClock.h"
+#import "FTestHelpers.h"
+#import "FPruneForest.h"
+#import "FTestCachePolicy.h"
+
+@interface FPruneForest (Test)
+
+- (FImmutableSortedDictionary *)pruneForest;
+
+@end
+
+@interface FTrackedQueryManagerTest : XCTestCase
+
+@end
+
+@implementation FTrackedQueryManagerTest
+
+#define SAMPLE_PARAMS \
+ ([[[[[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:PATH(@"child")]] \
+ startAt:[FSnapshotUtilities nodeFrom:@"startVal"] childKey:@"startKey"] \
+ endAt:[FSnapshotUtilities nodeFrom:@"endVal"] childKey:@"endKey"] \
+ limitToLast:5])
+
+#define SAMPLE_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:SAMPLE_PARAMS])
+
+#define DEFAULT_FOO_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:[FQueryParams defaultInstance]])
+
+#define DEFAULT_BAR_QUERY \
+ ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"bar"] params:[FQueryParams defaultInstance]])
+
+- (FTrackedQueryManager *)newManager {
+ return [self newManagerWithClock:[FSystemClock clock]];
+}
+
+- (FTrackedQueryManager *)newManagerWithClock:(id<FClock>)clock {
+ return [[FTrackedQueryManager alloc] initWithStorageEngine:[[FMockStorageEngine alloc] init]
+ clock:clock];
+}
+
+- (FTrackedQueryManager *)newManagerWithStorageEngine:(id<FStorageEngine>)storageEngine {
+ return [[FTrackedQueryManager alloc] initWithStorageEngine:storageEngine clock:[FSystemClock clock]];
+}
+
+- (void)testFindTrackedQuery {
+ FTrackedQueryManager *manager = [self newManager];
+ XCTAssertNil([manager findTrackedQuery:SAMPLE_QUERY]);
+ [manager setQueryActive:SAMPLE_QUERY];
+ XCTAssertNotNil([manager findTrackedQuery:SAMPLE_QUERY]);
+}
+
+- (void)testRemoveTrackedQuery {
+ FTrackedQueryManager *manager = [self newManager];
+ [manager setQueryActive:SAMPLE_QUERY];
+ XCTAssertNotNil([manager findTrackedQuery:SAMPLE_QUERY]);
+ [manager removeTrackedQuery:SAMPLE_QUERY];
+ XCTAssertNil([manager findTrackedQuery:SAMPLE_QUERY]);
+ [manager verifyCache];
+}
+
+- (void)testSetQueryActiveAndInactive {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ FTrackedQuery *q = [manager findTrackedQuery:SAMPLE_QUERY];
+ XCTAssertTrue(q.isActive);
+ XCTAssertEqual(q.lastUse, clock.currentTime);
+ [manager verifyCache];
+
+ [clock tick];
+ [manager setQueryInactive:SAMPLE_QUERY];
+ q = [manager findTrackedQuery:SAMPLE_QUERY];
+ XCTAssertFalse(q.isActive);
+ XCTAssertEqual(q.lastUse, clock.currentTime);
+ [manager verifyCache];
+}
+
+- (void)testSetQueryComplete {
+ FTrackedQueryManager *manager = [self newManager];
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryComplete:SAMPLE_QUERY];
+ XCTAssertTrue([manager findTrackedQuery:SAMPLE_QUERY].isComplete);
+ [manager verifyCache];
+}
+
+- (void)testSetQueriesComplete {
+ FTrackedQueryManager *manager = [self newManager];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"elsewhere")]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/baz") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"elsewhere") params:SAMPLE_PARAMS]];
+
+ [manager setQueriesCompleteAtPath:PATH(@"foo")];
+
+ XCTAssertTrue([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]].isComplete);
+ XCTAssertTrue([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]].isComplete);
+ XCTAssertTrue([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:SAMPLE_PARAMS]].isComplete);
+ XCTAssertTrue([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"foo/baz") params:SAMPLE_PARAMS]].isComplete);
+ XCTAssertFalse([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"elsewhere")]].isComplete);
+ XCTAssertFalse([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"elsewhere") params:SAMPLE_PARAMS]].isComplete);
+ [manager verifyCache];
+}
+
+- (void)testIsQueryComplete {
+ FTrackedQueryManager *manager = [self newManager];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryComplete:SAMPLE_QUERY];
+
+ [manager setQueryActive:DEFAULT_BAR_QUERY];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]];
+ [manager setQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]];
+
+ XCTAssertTrue([manager isQueryComplete:SAMPLE_QUERY]);
+ XCTAssertFalse([manager isQueryComplete:DEFAULT_BAR_QUERY]);
+
+ XCTAssertFalse([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"")]]);
+ XCTAssertTrue([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]]);
+ XCTAssertTrue([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz/quu")]]);
+}
+
+- (void)testPruneOldQueries {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"active1")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"active2")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"pinned1")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"pinned2")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive1")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive1")]];
+ [clock tick];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive2")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive2")]];
+ [clock tick];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive3")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive3")]];
+ [clock tick];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive4")]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive4")]];
+ [clock tick];
+
+ // Should remove the first two inactive queries
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.5 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"active1", @"active2", @"pinned1", @"pinned2", @"inactive3", @"inactive4"]
+ pathsToPrune:@[@"inactive1", @"inactive2"]];
+
+ // Should remove the other two inactive queries
+ forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"active1", @"active2", @"pinned1", @"pinned2"]
+ pathsToPrune:@[@"inactive3", @"inactive4"]];
+
+ // Nothing left to prune
+ forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1 maxQueries:NSUIntegerMax]];
+ XCTAssertFalse([forest prunesAnything]);
+
+ [manager verifyCache];
+}
+
+- (void) testPruneQueriesOverMaxSize {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ for (NSUInteger i = 0; i < 10; i++) {
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [clock tick];
+ }
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.2 maxQueries:6]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"4", @"5", @"6", @"7", @"8", @"9"]
+ pathsToPrune:@[@"0", @"1", @"2", @"3"]];
+}
+
+- (void) testPruneDefaultWithDeeperQueries {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1.0 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest pathsToKeep:@[@"foo/a", @"foo/b"] pathsToPrune:@[@"foo"]];
+ [manager verifyCache];
+}
+
+- (void) testPruneQueriesWithDefaultQueryOnParent {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]];
+ [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]];
+ [manager setQueryInactive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]];
+ [manager setQueryInactive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]];
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1.0 maxQueries:NSUIntegerMax]];
+ [self checkPruneForest:forest pathsToKeep:@[@"foo"] pathsToPrune:@[]];
+ [manager verifyCache];
+}
+
+- (void) testPruneQueriesOverMaxSizeUsingPercent {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ for (NSUInteger i = 0; i < 10; i++) {
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]];
+ [clock tick];
+ }
+
+ FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.6 maxQueries:6]];
+ [self checkPruneForest:forest
+ pathsToKeep:@[@"6", @"7", @"8", @"9"]
+ pathsToPrune:@[@"0", @"1", @"2", @"3", @"4", @"5"]];
+}
+
+- (void)checkPruneForest:(FPruneForest *)pruneForest pathsToKeep:(NSArray *)toKeep pathsToPrune:(NSArray *)toPrune {
+ FPruneForest *checkForest = [FPruneForest empty];
+ for (NSString *path in toPrune) {
+ checkForest = [checkForest prunePath:PATH(path)];
+ }
+ for (NSString *path in toKeep) {
+ checkForest = [checkForest keepPath:PATH(path)];
+ }
+ XCTAssertEqualObjects([pruneForest pruneForest], [checkForest pruneForest]);
+}
+
+- (void)testKnownCompleteChildren {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithStorageEngine:engine];
+
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo")], [NSSet set]);
+
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/a")]];
+ [manager setQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"foo/a")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/not-included")]];
+ [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/deep/not-included")]];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ FTrackedQuery *query = [manager findTrackedQuery:SAMPLE_QUERY];
+ [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"d", @"e"]] forQueryId:query.queryId];
+
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo")], ([NSSet setWithArray:@[@"a", @"d", @"e"]]));
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"")], [NSSet set]);
+ XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo/baz")], [NSSet set]);
+}
+
+- (void)testEnsureTrackedQueryForNewQuery {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager ensureCompleteTrackedQueryAtPath:PATH(@"foo")];
+ FTrackedQuery *query = [manager findTrackedQuery:DEFAULT_FOO_QUERY];
+ XCTAssertTrue(query.isComplete);
+ XCTAssertEqual(query.lastUse, clock.currentTime);
+}
+
+- (void)testEnsureTrackedQueryForAlreadyTrackedQuery {
+ FTestClock *clock = [[FTestClock alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithClock:clock];
+
+ [manager setQueryActive:DEFAULT_FOO_QUERY];
+
+ NSTimeInterval lastTick = clock.currentTime;
+ [clock tick];
+ [manager ensureCompleteTrackedQueryAtPath:PATH(@"foo")];
+ XCTAssertEqual([manager findTrackedQuery:DEFAULT_FOO_QUERY].lastUse, lastTick);
+}
+
+- (void)testHasActiveDefaultQuery {
+ FTrackedQueryManager *manager = [self newManager];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryActive:DEFAULT_BAR_QUERY];
+ XCTAssertFalse([manager hasActiveDefaultQueryAtPath:PATH(@"foo")]);
+ XCTAssertFalse([manager hasActiveDefaultQueryAtPath:PATH(@"")]);
+ XCTAssertTrue([manager hasActiveDefaultQueryAtPath:PATH(@"bar")]);
+ XCTAssertTrue([manager hasActiveDefaultQueryAtPath:PATH(@"bar/baz")]);
+}
+
+- (void)testCacheSanity {
+ FMockStorageEngine *engine = [[FMockStorageEngine alloc] init];
+ FTrackedQueryManager *manager = [self newManagerWithStorageEngine:engine];
+
+ [manager setQueryActive:SAMPLE_QUERY];
+ [manager setQueryActive:DEFAULT_FOO_QUERY];
+ [manager verifyCache];
+
+ [manager setQueryComplete:SAMPLE_QUERY];
+ [manager verifyCache];
+
+ [manager setQueryInactive:DEFAULT_FOO_QUERY];
+ [manager verifyCache];
+
+ FTrackedQueryManager *manager2 = [self newManagerWithStorageEngine:engine];
+ XCTAssertNotNil([manager2 findTrackedQuery:SAMPLE_QUERY]);
+ XCTAssertNotNil([manager2 findTrackedQuery:DEFAULT_FOO_QUERY]);
+ [manager2 verifyCache];
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m b/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m
new file mode 100644
index 0000000..6aee84d
--- /dev/null
+++ b/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m
@@ -0,0 +1,574 @@
+/*
+ * 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 <XCTest/XCTest.h>
+
+#import "FTreeSortedDictionary.h"
+#import "FLLRBNode.h"
+#import "FLLRBEmptyNode.h"
+#import "FLLRBValueNode.h"
+
+@interface FLLRBValueNode (Tests)
+- (id<FLLRBNode>) rotateLeft;
+- (id<FLLRBNode>) rotateRight;
+@end
+
+@interface FTreeSortedDictionaryTests : XCTestCase
+
+@end
+
+@implementation FTreeSortedDictionaryTests
+
+- (NSComparator) defaultComparator {
+ return ^(id obj1, id obj2) {
+ if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) {
+ return [obj1 compare:obj2];
+ }
+ else {
+ if(obj1 < obj2) {
+ return (NSComparisonResult)NSOrderedAscending;
+ }
+ else if (obj1 > obj2) {
+ return (NSComparisonResult)NSOrderedDescending;
+ }
+ else {
+ return (NSComparisonResult)NSOrderedSame;
+ }
+ }
+ };
+}
+
+- (void)testCreateNode
+{
+ FTreeSortedDictionary* map = [[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertTrue([map.root.left isEmpty], @"Left child is properly empty");
+ XCTAssertTrue([map.root.right isEmpty], @"Right child is properly empty");
+}
+
+- (void)testGetNilReturnsNil {
+ FImmutableSortedDictionary *map1 = [[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map1 get:nil]);
+
+ FImmutableSortedDictionary *map2 = [[[FTreeSortedDictionary alloc] initWithComparator:^NSComparisonResult(id obj1, id obj2) {
+ return [obj1 compare:obj2];
+ }]
+ insertKey:@"key" withValue:@"value"];
+ XCTAssertNil([map2 get:nil]);
+}
+
+- (void)testSearchForSpecificKey {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+ XCTAssertNil([map get:@3], @"Properly not found object");
+}
+
+- (void)testInsertNewKeyValuePair {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ XCTAssertEqualObjects(map.root.key, @2, @"Check the root key");
+ XCTAssertEqualObjects(map.root.left.key, @1, @"Check the root.left key");
+}
+
+- (void)testRemoveKeyValuePair {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2];
+
+ FImmutableSortedDictionary* newMap = [map removeKey:@1];
+ XCTAssertEqualObjects([newMap get:@2], @2, @"Found second object");
+ XCTAssertNil([newMap get:@1], @"Properly not found object");
+
+ // Make sure the original one is not mutated
+ XCTAssertEqualObjects([map get:@1], @1, @"Found first object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found second object");
+}
+
+- (void)testMoreRemovals {
+ FTreeSortedDictionary* map = [[[[[[[[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9]
+ insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2]
+ insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88];
+ XCTAssertNotNil([map get:@7], @"Found object");
+ XCTAssertNotNil([map get:@3], @"Found object");
+ XCTAssertNotNil([map get:@1], @"Found object");
+
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@7];
+ FImmutableSortedDictionary* m2 = [map removeKey:@3];
+ FImmutableSortedDictionary* m3 = [map removeKey:@1];
+
+ XCTAssertNil([m1 get:@7], @"Removed object");
+ XCTAssertNotNil([m1 get:@3], @"Found object");
+ XCTAssertNotNil([m1 get:@1], @"Found object");
+
+ XCTAssertNil([m2 get:@3], @"Removed object");
+ XCTAssertNotNil([m2 get:@7], @"Found object");
+ XCTAssertNotNil([m2 get:@1], @"Found object");
+
+
+ XCTAssertNil([m3 get:@1], @"Removed object");
+ XCTAssertNotNil([m3 get:@7], @"Found object");
+ XCTAssertNotNil([m3 get:@3], @"Found object");
+}
+
+- (void) testRemovalBug {
+ FTreeSortedDictionary* map = [[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2]
+ insertKey:@3 withValue:@3];
+
+ XCTAssertEqualObjects([map get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([map get:@2], @2, @"Found object");
+ XCTAssertEqualObjects([map get:@3], @3, @"Found object");
+
+ FImmutableSortedDictionary* m1 = [map removeKey:@2];
+ XCTAssertEqualObjects([m1 get:@1], @1, @"Found object");
+ XCTAssertEqualObjects([m1 get:@3], @3, @"Found object");
+ XCTAssertNil([m1 get:@2], @"Removed object");
+}
+
+- (void) testIncreasing {
+ int total = 100;
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map insertKey:item withValue:item];
+ }
+
+ XCTAssertTrue([map count] == 100, @"Check if all 100 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+
+ for(int i = 0; i < total; i++) {
+ NSNumber* item = [NSNumber numberWithInt:i];
+ map = [map removeKey:item];
+ }
+
+ XCTAssertTrue([map count] == 0, @"Check if all 100 objects were removed");
+ // We can't check the depth here because the map no longer contains values, so we check that it doesn't responsd to this check
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBEmptyNode class]], @"Root is an empty node");
+ XCTAssertFalse([map respondsToSelector:@selector(checkMaxDepth)], @"The empty node doesn't respond to this selector.");
+}
+
+- (void) testStructureShouldBeValidAfterInsertionA {
+ FTreeSortedDictionary* map = [[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@2 withValue:@2]
+ insertKey:@3 withValue:@3];
+
+
+ XCTAssertEqualObjects(map.root.key, @2, @"Check root key");
+ XCTAssertEqualObjects(map.root.left.key, @1, @"Check the left key is correct");
+ XCTAssertEqualObjects(map.root.right.key, @3, @"Check the right key is correct");
+}
+
+- (void) testStructureShouldBeValidAfterInsertionB {
+ FTreeSortedDictionary* map = [[[[[[[[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9]
+ insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2]
+ insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88];
+
+ XCTAssertTrue([map count] == 12, @"Check if all 12 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+}
+
+- (void) testRotateLeftLeavesTreeInAValidState {
+ FLLRBValueNode* node = [[FLLRBValueNode alloc] initWithKey:@4 withValue:@4 withColor:BLACK withLeft:
+ [[FLLRBValueNode alloc] initWithKey:@2 withValue:@2 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc]initWithKey:@7 withValue:@7 withColor:RED withLeft:[[FLLRBValueNode alloc ]initWithKey:@5 withValue:@5 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc] initWithKey:@8 withValue:@8 withColor:BLACK withLeft:nil withRight:nil]]];
+
+ FLLRBValueNode* node2 = [node performSelector:@selector(rotateLeft)];
+
+ XCTAssertTrue([node2 count] == 5, @"Make sure the count is correct");
+ XCTAssertTrue([node2 checkMaxDepth], @"Check proper structure");
+}
+
+- (void) testRotateRightLeavesTreeInAValidState {
+ FLLRBValueNode* node = [[FLLRBValueNode alloc] initWithKey:@7 withValue:@7 withColor:BLACK withLeft:[[FLLRBValueNode alloc] initWithKey:@4 withValue:@4 withColor:RED withLeft:[[FLLRBValueNode alloc] initWithKey:@2 withValue:@2 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc] initWithKey:@5 withValue:@5 withColor:BLACK withLeft:nil withRight:nil]] withRight:[[FLLRBValueNode alloc] initWithKey:@8 withValue:@8 withColor:BLACK withLeft:nil withRight:nil]];
+
+ FLLRBValueNode* node2 = [node performSelector:@selector(rotateRight)];
+
+ XCTAssertTrue([node2 count] == 5, @"Make sure the count is correct");
+ XCTAssertEqualObjects(node2.key, @4, @"Check roots key");
+ XCTAssertEqualObjects(node2.left.key, @2, @"Check first left child key");
+ XCTAssertEqualObjects(node2.right.key, @7, @"Check first right child key");
+ XCTAssertEqualObjects(node2.right.left.key, @5, @"Check second right left key");
+ XCTAssertEqualObjects(node2.right.right.key, @8, @"Check second right left key");
+}
+
+- (void) testStructureShouldBeValidAfterInsertionC {
+ FTreeSortedDictionary* map = [[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9];
+
+ XCTAssertTrue([map count] == 6, @"Check if all 6 objects are in the map");
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+
+ FTreeSortedDictionary* m2 = [[[map insertKey:@20 withValue:@20]
+ insertKey:@18 withValue:@18]
+ insertKey:@2 withValue:@2];
+ XCTAssertTrue([m2 count] == 9, @"Check if all 9 objects are in the map");
+ XCTAssertTrue([m2.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)m2.root checkMaxDepth], @"Checking valid depth and tree structure");
+
+ FTreeSortedDictionary* m3 = [[[[m2 insertKey:@71 withValue:@71]
+ insertKey:@42 withValue:@42]
+ insertKey:@88 withValue:@88]
+ insertKey:@20 withValue:@20]; // Add a dupe to see if the size is correct
+ XCTAssertTrue([m3 count] == 12, @"Check if all 12 (minus dupe @20) objects are in the map");
+ XCTAssertTrue([m3.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)m3.root checkMaxDepth], @"Checking valid depth and tree structure");
+}
+
+- (void) testOverride {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ insertKey:@10 withValue:@8];
+
+ XCTAssertEqualObjects([map get:@10], @8, @"Found first object");
+}
+- (void) testEmpty {
+ FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@10 withValue:@10]
+ removeKey:@10];
+
+ XCTAssertTrue([map isEmpty], @"Properly empty");
+
+}
+
+- (void) testEmptyGet {
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertNil([map get:@"something"], @"Properly nil");
+}
+
+- (void) testEmptyCount {
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([map count] == 0, @"Properly zero count");
+}
+
+- (void) testEmptyRemoval {
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+ XCTAssertTrue([[map removeKey:@"sometjhing"] count] == 0, @"Properly zero count");
+}
+
+- (void) testReverseTraversal {
+ FTreeSortedDictionary* map = [[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@5 withValue:@5]
+ insertKey:@3 withValue:@3]
+ insertKey:@2 withValue:@2]
+ insertKey:@4 withValue:@4];
+
+ __block int next = 5;
+ [map enumerateKeysAndObjectsReverse:YES usingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Properly equal");
+ next = next - 1;
+ }];
+}
+
+
+- (void) testInsertionAndRemovalOfAHundredItems {
+ int N = 100;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+ NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ [toRemove addObject:[NSNumber numberWithInt:i]];
+ }
+
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, N, @"Check we traversed all of the items");
+
+ // remove them
+
+ for(int i = 0; i < N; i++) {
+ if([map.root isMemberOfClass:[FLLRBValueNode class]]) {
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ map = [map removeKey:[toRemove objectAtIndex:i]];
+ }
+
+
+ XCTAssertEqual([map count], 0, @"Check we removed all of the items");
+}
+
+- (void) shuffleArray:(NSMutableArray *)array {
+ NSUInteger count = [array count];
+ for(NSUInteger i = 0; i < count; i++) {
+ NSInteger nElements = count - i;
+ NSInteger n = (arc4random() % nElements) + i;
+ [array exchangeObjectAtIndex:i withObjectAtIndex:n];
+ }
+}
+
+- (void) testBalanceProblem {
+
+ NSArray* toInsert = [[NSArray alloc] initWithObjects:@1,@7,@8,@5,@2,@6,@4,@0,@3, nil];
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < [toInsert count]; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue([map count] == [toInsert count], @"Check if all N objects are in the map");
+
+ // check the order is correct
+ __block int next = 0;
+ [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
+ XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key");
+ XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value");
+ next = next + 1;
+ }];
+ XCTAssertEqual(next, [[NSNumber numberWithUnsignedInteger:[toInsert count]] intValue], @"Check we traversed all of the items");
+
+ // removing one triggers the balance problem
+
+ map = [map removeKey:@5];
+
+ if([map.root isMemberOfClass:[FLLRBValueNode class]]) {
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+}
+
+- (void) testPredecessorKey {
+ FTreeSortedDictionary* map = [[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]
+ insertKey:@1 withValue:@1]
+ insertKey:@50 withValue:@50]
+ insertKey:@3 withValue:@3]
+ insertKey:@4 withValue:@4]
+ insertKey:@7 withValue:@7]
+ insertKey:@9 withValue:@9];
+
+ XCTAssertNil([map getPredecessorKey:@1], @"First object doesn't have a predecessor");
+ XCTAssertEqualObjects([map getPredecessorKey:@3], @1, @"@1");
+ XCTAssertEqualObjects([map getPredecessorKey:@4], @3, @"@3");
+ XCTAssertEqualObjects([map getPredecessorKey:@7], @4, @"@4");
+ XCTAssertEqualObjects([map getPredecessorKey:@9], @7, @"@7");
+ XCTAssertEqualObjects([map getPredecessorKey:@50], @9, @"@9");
+ XCTAssertThrows([map getPredecessorKey:@777], @"Expect exception about nonexistant key");
+}
+
+- (void) testEnumerator {
+ int N = 100;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+ NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ [toRemove addObject:[NSNumber numberWithInt:i]];
+ }
+
+
+ [self shuffleArray:toInsert];
+ [self shuffleArray:toRemove];
+
+ FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node");
+ XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure");
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+
+ NSEnumerator* enumerator = [map keyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = 0;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 1;
+ }
+}
+
+- (void) testReverseEnumerator {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ NSEnumerator* enumerator = [map reverseKeyEnumerator];
+ id next = [enumerator nextObject];
+ int correctValue = N - 1;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue--;
+ }
+}
+
+- (void) testEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 12;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map keyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue + 2;
+ }
+ }
+}
+
+- (void) testReverseEnumeratorFrom {
+ int N = 20;
+ NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N];
+
+ for(int i = 0; i < N; i++) {
+ [toInsert addObject:[NSNumber numberWithInt:i*2]];
+ }
+
+ [self shuffleArray:toInsert];
+
+ FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]];
+
+ // add them to the dictionary
+ for(int i = 0; i < N; i++) {
+ map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]];
+ }
+ XCTAssertTrue([map count] == N, @"Check if all N objects are in the map");
+ XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary");
+
+ // Test from inbetween keys
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@11];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+
+ // Test from key in map
+ {
+ NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@10];
+ id next = [enumerator nextObject];
+ int correctValue = 10;
+ while(next != nil) {
+ XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key");
+ next = [enumerator nextObject];
+ correctValue = correctValue - 2;
+ }
+ }
+}
+
+@end
diff --git a/Example/Database/Tests/Unit/FUtilitiesTest.m b/Example/Database/Tests/Unit/FUtilitiesTest.m
new file mode 100644
index 0000000..a012250
--- /dev/null
+++ b/Example/Database/Tests/Unit/FUtilitiesTest.m
@@ -0,0 +1,116 @@
+/*
+ * 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 <UIKit/UIKit.h>
+#import <XCTest/XCTest.h>
+#import "FUtilities.h"
+#import "FIRDatabase_Private.h"
+#import "FIRDatabaseReference_Private.h"
+#import "FClock.h"
+#import "FIRDatabaseConfig_Private.h"
+#import "FWebSocketConnection.h"
+#import "FConstants.h"
+
+@interface FWebSocketConnection (Tests)
+- (NSString*)userAgent;
+@end
+
+@interface FUtilitiesTest : XCTestCase
+
+@end
+
+@implementation FUtilitiesTest
+
+- (void)testUrlWithSchema {
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:@"https://repo.firebaseio.com"];
+ XCTAssertEqualObjects(parsedUrl.repoInfo.host, @"repo.firebaseio.com");
+ XCTAssertEqualObjects(parsedUrl.repoInfo.namespace, @"repo");
+ XCTAssertTrue(parsedUrl.repoInfo.secure);
+ XCTAssertEqualObjects(parsedUrl.path, [FPath empty]);
+}
+
+- (void)testUrlParsedWithoutSchema {
+ FParsedUrl *parsedUrl = [FUtilities parseUrl:@"repo.firebaseio.com"];
+ XCTAssertEqualObjects(parsedUrl.repoInfo.host, @"repo.firebaseio.com");
+ XCTAssertEqualObjects(parsedUrl.repoInfo.namespace, @"repo");
+ XCTAssertTrue(parsedUrl.repoInfo.secure);
+ XCTAssertEqualObjects(parsedUrl.path, [FPath empty]);
+}
+
+- (void)testDefaultCacheSizeIs10MB {
+ XCTAssertEqual([FIRDatabaseReference defaultConfig].persistenceCacheSizeBytes, (NSUInteger)10*1024*1024);
+ XCTAssertEqual([FIRDatabaseConfig configForName:@"test-config"].persistenceCacheSizeBytes, (NSUInteger)10*1024*1024);
+}
+
+- (void)testSettingCacheSizeToHighOrToLowThrows {
+ FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:@"config-tests-config"];
+ config.persistenceCacheSizeBytes = 5*1024*1024; // Works fine
+ XCTAssertThrows(config.persistenceCacheSizeBytes = (1024*1024-1));
+ XCTAssertThrows(config.persistenceCacheSizeBytes = 100*1024*1024+1);
+}
+
+- (void)testSystemClockMatchesCurrentTime {
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
+ // Accuracy within 10ms
+ XCTAssertEqualWithAccuracy(currentTime, [[FSystemClock clock] currentTime], 0.010);
+}
+
+// This test is here for a lack of a better place to put it
+- (void)testUserAgentString {
+ FWebSocketConnection *conn = [[FWebSocketConnection alloc] init];
+
+ NSString *agent = [conn performSelector:@selector(userAgent) withObject:nil];
+
+ NSArray *parts = [agent componentsSeparatedByString:@"/"];
+ XCTAssertEqual(parts.count, (NSUInteger)5);
+ XCTAssertEqualObjects(parts[0], @"Firebase");
+ XCTAssertEqualObjects(parts[1], kWebsocketProtocolVersion); // Wire protocol version
+ XCTAssertEqualObjects(parts[2], [FIRDatabase buildVersion]); // Build version
+ XCTAssertEqualObjects(parts[3], [[UIDevice currentDevice] systemVersion]); // iOS Version
+#if TARGET_OS_IPHONE
+ NSString *deviceName = [UIDevice currentDevice].model;
+ XCTAssertEqualObjects([parts[4] componentsSeparatedByString:@"_"][0], deviceName);
+#endif
+
+}
+
+- (void)testKeyComparison {
+ NSArray *order = @[
+ @"-2147483648", @"0", @"1", @"2", @"10", @"2147483647", // Treated as integers
+ @"-2147483649", @"-2147483650", @"-a", @"2147483648", @"21474836480", @"2147483649", @"a" // treated as strings
+ ];
+ for (NSInteger i = 0; i < order.count; i++) {
+ for (NSInteger j = i + 1; j < order.count; j++) {
+ NSString *first = order[i];
+ NSString *second = order[j];
+ XCTAssertEqual([FUtilities compareKey:first toKey:second], NSOrderedAscending,
+ @"Expected %@ < %@", first, second);
+ XCTAssertEqual([FUtilities compareKey:first toKey:first], NSOrderedSame,
+ @"Expected %@ == %@", first, first);
+ XCTAssertEqual([FUtilities compareKey:second toKey:first], NSOrderedDescending,
+ @"Expected %@ > %@", second, first);
+ }
+ }
+}
+
+// Enforce a > b, b < a, a != b, because this is apparently something that happens semi-regularly
+- (void)testUnicodeKeyComparison {
+ XCTAssertEqual([FUtilities compareKey:@"유주연" toKey:@"윤규완오빠"], NSOrderedAscending);
+ XCTAssertEqual([FUtilities compareKey:@"윤규완오빠" toKey:@"유주연"], NSOrderedDescending);
+ XCTAssertNotEqual([FUtilities compareKey:@"윤규완오빠" toKey:@"유주연"], NSOrderedSame);
+}
+
+@end
diff --git a/Example/Database/Tests/en.lproj/InfoPlist.strings b/Example/Database/Tests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Example/Database/Tests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Example/Database/Tests/syncPointSpec.json b/Example/Database/Tests/syncPointSpec.json
new file mode 100644
index 0000000..f39d29d
--- /dev/null
+++ b/Example/Database/Tests/syncPointSpec.json
@@ -0,0 +1,8203 @@
+[
+ {
+ "name": "Default listen handles a parent set",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Now do a set at the parent. Expect only the 'a' child to get events",
+ "type": "set",
+ "path": "",
+ "data": {
+ "a": 1,
+ "b": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Default listen handles a set at the same level",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Do a set at the same level. Expect the full value to raise events",
+ "type": "set",
+ "path": "a",
+ "data": {
+ "foo": "bar",
+ "yes": true
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "yes",
+ "prevName": "foo",
+ "data": true
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ "yes": true
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "A query can get a complete cache then a merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 3,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "a",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "d": 4
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "b",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "d": 4
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "tag": 1,
+ "path": "a",
+ "data": {
+ "a": 5,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "d",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "a",
+ "prevName": null,
+ "data": 5
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": 5,
+ "b": 2,
+ "c": 3
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server merge on listener with complete children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a/b",
+ "data": {"c": 3, "d": 4},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Empty set doesn't prevent server updates",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo" : "bar"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "empty-path",
+ "data": null,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": { "foo": "new-bar" },
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "foo",
+ "prevName": null,
+ "data": "new-bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo" : "new-bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Deep merge on listener with complete children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "a/x/y/z",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/x/y/z",
+ "data": null,
+ "events": [
+ {
+ "path": "a/x/y/z",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a/x/y/z",
+ "data": {"c": 3, "d": 4},
+ ".comment": "No events for the top-level listener, since it's not a complete child",
+ "events": [
+ {
+ "path": "a/x/y/z",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/x/y/z",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a/x/y/z",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update child listener twice",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/c",
+ "data": "foo",
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": "foo"
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": "foo"}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": "foo"}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update child of default listen that already has a complete cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Fill the listen's cache so we can test a child set with an existing cache",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {
+ "b": 2,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": 2,
+ "c": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now do a set at a child, expect the child event and a value event",
+ "type": "set",
+ "path": "a/b",
+ "data": 4,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": 4,
+ "c": 3
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update child of default listen that has no cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Now do a set at a child, expect the child event only",
+ "type": "set",
+ "path": "a/b",
+ "data": 4,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update (via set) the child of a co-located default listener and query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "startAt": {"index": null, "name": "b"},
+ "endAt": {"index": null, "name": "g"}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Fill the cache. Since the default listener is there, no tag needed",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {
+ "a": 1,
+ "c": 3,
+ "d": 4
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "a",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ },
+ {
+ ".comment": "Cache is primed. Now do the child set",
+ "type": "set",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b":2, "c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b":2, "c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update (via set) the child of a query with a full cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "startAt": {"index": null, "name": "b"},
+ "endAt": {"index": null, "name": "g"}
+ },
+ "events": []
+ },
+ {
+ ".comment": "Fill the cache first",
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": {
+ "c": 3,
+ "d": 4
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"c": 3, "d": 4}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update (via set) a child below an empty query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "startAt": {"name": "b", "index": null},
+ "endAt": {"name": "g", "index": null}
+ },
+ "events": []
+ },
+ {
+ ".comment": "Set a single child, outside the window",
+ "type": "set",
+ "path": "a/h",
+ "data": 8,
+ "events": []
+ },
+ {
+ ".comment": "Now set a single child inside the window",
+ "type": "set",
+ "path": "a/e",
+ "data": 5,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "e",
+ "prevName": null,
+ "data": 5
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update descendant of default listener with full cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Fill the cache",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {
+ "b": {
+ "d": 4
+ },
+ "e": 5
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "d": 4
+ }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "e",
+ "prevName": "b",
+ "data": 5
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {
+ "d": 4
+ },
+ "e": 5
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now do a set at a/b/c, expect child event + new value event",
+ "type": "set",
+ "path": "a/b/c",
+ "data": 3,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": 3,
+ "d": 4
+ }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {
+ "c": 3,
+ "d": 4
+ },
+ "e": 5
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Descendant set below an empty default listener is ignored",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Now do a set at a/b/c, expect no events",
+ "type": "set",
+ "path": "a/b/c",
+ "data": 3,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Update of a child. This can happen if a child listener is added and removed",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set with only child caches",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Can revert a duplicate child set",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ },
+ {
+ ".comment": "This set duplicates the data in the previous one, so no events expected",
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the second set should have no effect, as the underlying set still exists",
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "revert": true,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Can revert a child set and see the underlying data",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 4,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 3,
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the second set should make the underlying set visible again, as it is now confirmed",
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "revert": true,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert child set with no server data",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": {"d": 4, "e": 5},
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"d": 4, "e": 5}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/c",
+ "data": {"z": 26},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {"z": 26}
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": {"d": 4, "e": 5}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert deep set with no server data",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a/b/c",
+ "data": {"d": 4, "e": 5},
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/x/y",
+ "data": {"z": 26},
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set covered by non-visible transaction",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "Initial server value is X.",
+ "type": "serverUpdate",
+ "path": "",
+ "data": "X",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "X"
+ }
+ ]
+ },
+ {
+ ".comment": "Set to Y.",
+ "type": "set",
+ "path": "",
+ "data": "Y",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "Y"
+ }
+ ]
+ },
+ {
+ ".comment": "Overwrite with a non-visible 'transaction'.",
+ "type": "set",
+ "path": "",
+ "data": "Z",
+ "visible": false,
+ "events": []
+ },
+ {
+ ".comment": "Revert set to Y (e.g. security failed), so we should see it go back to Y.",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "X"
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Clear parent shadowing server values set with server children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a",
+ "data": {"b": 28, "c": 3},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "This listen should get a complete event snap, as well as complete server children",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 28
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 28, "c": 3}
+ }
+ ]
+ },
+ {
+ ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 29,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the set should result in updated values for b",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 29, "c": 3}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Clear child shadowing server values set with server children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 28,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "This listen should get an event child snap, as well as a complete server child: b",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 29,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the set should result in no events. We don't yet have the server data at the parent",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 29
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated merge doesn't shadow server updates",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a",
+ "data": {"b": 2, "c": 3},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3}
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/d",
+ "data": 4,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": 4
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3, "d": 4}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Can set alongside a remote merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a",
+ "data": {"b": 28, "c": 3},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2, "c": 3}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "setPriority on a location with no cache",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/.priority",
+ "data": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": "bar",
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": { ".priority": "foo", ".value": "bar" }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "deep update deletes child from limit window and pulls in new child",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a",
+ "data": {
+ "a": {"aa": 2, "aaa": 3},
+ "b": {"bb": 2, "bbb": 3},
+ "c": {"cc": 2, "ccc": 3}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "limitToLast": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"bb": 2, "bbb": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {"cc": 2, "ccc": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {"bb": 2, "bbb": 3},
+ "c": {"cc": 2, "ccc": 3}
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a/b",
+ "data": {
+ "bb": null,
+ "bbb": null
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": {"bb": 2, "bbb": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {"aa": 2, "aaa": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": {"aa": 2, "aaa": 3},
+ "c": {"cc": 2, "ccc": 3}
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "deep set deletes child from limit window and pulls in new child",
+ "steps": [
+ {
+ "type": "set",
+ "path": "a",
+ "data": {
+ "a": {"aa": 2},
+ "b": {"bb": 2},
+ "c": {"cc": 2}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "tag": 1,
+ "limitToLast": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"bb": 2}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {"cc": 2}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {"bb": 2},
+ "c": {"cc": 2}
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/bb",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": {"bb": 2}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {"aa": 2}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "a": {"aa": 2},
+ "c": {"cc": 2}
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Edge case in newChildForChange_",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/d",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/b/c",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/d",
+ "data": 4,
+ "events": [
+ {
+ "type": "value",
+ "path": "a/d",
+ "data": 4
+ },
+ {
+ "type": "child_added",
+ "path": "a",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b/c",
+ "data": 3,
+ "events": [
+ {
+ "path": "a/b/c",
+ "type": "value",
+ "data": 3
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set in query window",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "limitToLast": 1,
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": {"b": 2},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/c",
+ "data": 3,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "b",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"c": 3}
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "revert": true,
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "c",
+ "data": 3
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Handles a server value moving a child out of a query window",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 4}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}}
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1,
+ "orderBy": "value"
+ },
+ "path": "a/b",
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 4}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": {"value": 4}}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/d/value",
+ "data": 5,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_moved",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "child_changed",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": {"value": 5}}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 5}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 5}}}
+ }
+ ]
+ },
+ {
+ ".comment": "The query is shadowed, so only one data update arrives. We're simulating a server value, so it's different than what was set",
+ "type": "serverUpdate",
+ "path": "a/b/d/value",
+ "data": 2,
+ "events": []
+ },
+ {
+ ".comment": "Now that we're acking the write, we should see the effect of the change",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": {"value": 3}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": {"value": 3}}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 2}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 2}}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Update of indexed child works",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 3}, "d": {"value": 4}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}}
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1,
+ "orderBy": "value"
+ },
+ "path": "a/b",
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": {"value": 4}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": {"value": 4}}
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a/b/c",
+ "data": {"value": 5},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": {"value": 4}
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": {"value": 5}
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": {"value": 5}}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {"c": {"value": 5}, "d": {"value": 4}}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": {"c": {"value": 5}, "d": {"value": 4}}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Merge applied to empty limit",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "params": {
+ "limitToLast": 1,
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a",
+ "data": {"b": 1},
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"b": 1}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit is refilled from server data after merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/b",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": 4}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}}
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a/b",
+ "data": {"d": null},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Handle repeated listen with merge as first update",
+ "steps": [
+ {
+ ".comment": "Assume that we just unlistened on this path, and before the unlisten arrives, a merge was sent by the server",
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "This happens when a merge arriving from the server while the 2nd listen is in flight",
+ "type": "serverMerge",
+ "path": "a",
+ "data": {"c": 3},
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Limit is refilled from server data after set",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/b",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"d": 4}
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3, "d": 4}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3, "d": 4}}
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/b/d",
+ "data": null,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_removed",
+ "name": "d",
+ "data": 4
+ },
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": "a",
+ "data": {"c": 3}
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"a": 1, "b": {"c": 3}}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "query on weird path.",
+ ".comment": "We used to use '|' as a separator, which broke with paths containing |",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "params": {
+ "tag": 1,
+ "limitToLast": 5
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "tag": 1,
+ "data": { "a": "a" },
+ "events": [
+ {
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": "a"
+ },
+ {
+ "path": "foo|!@%^&*()_<>?+={}blah",
+ "type": "value",
+ "data": { "a": "a" }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "runs, round2",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": "baz",
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": "baz"
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "foo/new",
+ "data": "bar",
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "new",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "new" : "bar"}
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": "baz",
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": { "new" : "bar"},
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": { "new" : true, "other" : "bar"},
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "other",
+ "prevName": "new",
+ "data": "bar"
+ },
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "new",
+ "prevName": null,
+ "data": true
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "new": true, "other": "bar"}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "handles nested listens",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo/bar",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": true,
+ "d": false
+ }
+ },
+ "baz": false
+ },
+ "events": [
+ {
+ "path": "foo/bar",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": true
+ },
+ {
+ "path": "foo/bar",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": false
+ },
+ {
+ "path": "foo/bar",
+ "type": "value",
+ "data": {"c": true, "d": false}
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "bar",
+ "prevName": "b",
+ "data": {"c": true, "d": false}
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": true,
+ "d": false
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": false,
+ "d": false,
+ "e": true
+ },
+ "f": 3
+ },
+ "baz": false
+ },
+ "events": [
+ {
+ "path": "foo/bar",
+ "type": "child_added",
+ "name": "e",
+ "prevName": "d",
+ "data": true
+ },
+ {
+ "path": "foo/bar",
+ "type": "child_changed",
+ "name": "c",
+ "prevName": null,
+ "data": false
+ },
+ {
+ "path": "foo/bar",
+ "type": "value",
+ "data": {
+ "c": false,
+ "d": false,
+ "e": true
+ }
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "f",
+ "prevName": "bar",
+ "data": 3
+ },
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "bar",
+ "prevName": "b",
+ "data": {
+ "c": false,
+ "d": false,
+ "e": true
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": false,
+ "d": false,
+ "e": true
+ },
+ "f": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Duplicate set, no events raised",
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2,
+ "bar": {
+ "c": false,
+ "d": false,
+ "e": true
+ },
+ "f": 3
+ },
+ "baz": false
+ },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Handles a set below a listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": 1,
+ ".comment": "We only expect a child_added, since it does not completely fill the view",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "does non-default queries",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "foo": {
+ "a": 1,
+ "b": 2
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "b": 2
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now have the server send the same data to the query. No events result because there is no change",
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "foo",
+ "data": {
+ "b": 2
+ },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "handles a co-located default listener and query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": { "a": 1, "b": 2},
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "a": 1, "b": 2}
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Default and non-default listener at same location with server update",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": {"a": 1, "b": 2},
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "data": 1,
+ "prevName": null
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "data": 2,
+ "prevName": "a"
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {"a": 1, "b": 2}
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "data": 2,
+ "prevName": null
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {"b": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Add a parent listener to a complete child listener, expect child event",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Add listens to a set, expect correct events, including a child event",
+ "steps": [
+ {
+ "type": "set",
+ "path": "foo",
+ "data": {"bar": 1, "baz": 2},
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo/bar",
+ "events": [
+ {
+ "path": "foo/bar",
+ "type": "value",
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": {"bar": 1, "baz": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "ServerUpdate to a child listener raises child events at parent",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "ServerUpdate to a child listener raises child events at parent query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Multiple complete children are handled properly",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo/a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo/b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo/a",
+ "data": 1,
+ "events": [
+ {
+ "path": "foo/a",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "foo/b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Write leaf node, overwrite at parent node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/aa",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/aa",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a",
+ "data": {
+ "aa": 2
+ },
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "aa",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 2
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Confirm complete children from the server",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/aa",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/aa",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ ".comment": "At some point in the future, we might consider sending a hash here to avoid duplicate data",
+ "data": {"aa": 1},
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"aa": 1}
+ }
+ ]
+ },
+ {
+ ".comment": "Now, delete the same child and make sure we get the right events",
+ "type": "serverUpdate",
+ "path": "a/aa",
+ "data": null,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": null
+ },
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Write leaf, overwrite from parent",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/aa",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/bb",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "First set is at leaf. Expect only a child_added for the parent, nothing for the sibling",
+ "type": "set",
+ "path": "a/aa",
+ "data": 1,
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "aa",
+ "prevName": null,
+ "data": 1
+ }
+ ]
+ },
+ {
+ ".comment": "Now set at the parent. Expect value events for everyone",
+ "type": "set",
+ "path": "a",
+ "data": {"aa": 2},
+ "events": [
+ {
+ "path": "a/aa",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "a/bb",
+ "type": "value",
+ "data": null
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "aa",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {"aa": 2}
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Basic update test",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "b",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "Initial data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": 1
+ },
+ {
+ "path": "b",
+ "type": "value",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now update two children. Not b, there should be no events at b",
+ "type": "update",
+ "path": "",
+ "data": {
+ "a": true,
+ "c": false
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": true
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "a",
+ "prevName": null,
+ "data": true
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "c",
+ "prevName": "b",
+ "data": false
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": true,
+ "b": 2,
+ "c": false
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "No double value events for user ack",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToLast": 1,
+ "endAt": {"index": null, "name": "d"}
+ },
+ "events": []
+ },
+ {
+ ".comment": "user sets data",
+ "type": "set",
+ "path": "foo",
+ "data": {
+ "a": 1,
+ "b": 2,
+ "c": 3
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "c": 3 }
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ }
+ ]
+ },
+ {
+ ".comment": "server acks data, but local overwrite causes no events to fire",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": null,
+ "events": []
+ },
+ {
+ ".comment": "server sends data with merge",
+ "type": "serverMerge",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "c": 3
+ },
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo/d",
+ "data": 4,
+ "events": [
+ {
+ "path": "foo",
+ "type": "value",
+ "data": { "d": 4 }
+ },
+ {
+ "path": "foo",
+ "type": "child_removed",
+ "name": "c",
+ "prevName": null,
+ "data": 3
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": 4
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "d": 4
+ },
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "revert": false,
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Basic key index sanity check",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderByKey": true,
+ "startAt": { "index": "aa" },
+ "endAt": { "index": "e" }
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "a": { ".priority": 10, ".value": "a" },
+ "b": { ".priority": 5, ".value": "b" },
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" },
+ "f": { ".priority": 8, ".value": "f" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {".priority": 5, ".value": "b"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {".priority": 20, ".value": "c"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": {".priority": 7, ".value": "d"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "e",
+ "prevName": "d",
+ "data": {".priority": 30, ".value": "e"}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { ".priority": 5, ".value": "b" },
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Add a new item outside of range and make sure we get events.",
+ "type": "set",
+ "path": "a",
+ "data": "hello!",
+ "events": [ ]
+ },
+ {
+ ".comment": "Add a new item within range and ensure we get ld_added.",
+ "type": "set",
+ "path": "bass",
+ "data": 3.14,
+ "events":
+ [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "bass",
+ "prevName": "b",
+ "data": 3.14
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { ".priority": 5, ".value": "b" },
+ "bass": 3.14,
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Modify an item and ensure we get child_changed.",
+ "type": "set",
+ "path": "b",
+ "data": 42,
+ "events":
+ [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 42
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": 42,
+ "bass": 3.14,
+ "c": { ".priority": 20, ".value": "c" },
+ "d": { ".priority": 7, ".value": "d" },
+ "e": { ".priority": 30, ".value": "e" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Collect correct subviews to listen on",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "callbackId": 1,
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "callbackId": 1,
+ "path": "/a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "callbackId": 1,
+ "path": "/a/b",
+ "events": []
+ },
+ {
+ ".comment": "should not cause /a/b to be listened upon",
+ "type": "unlisten",
+ "callbackId": 1,
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "should now cause /a/b to be listened upon",
+ "type": "unlisten",
+ "callbackId": 1,
+ "path": "/a",
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Limit to first one on ordered query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "vanished",
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "triceratops": {"vanished": -66000000},
+ "stegosaurus": {"vanished": -155000000},
+ "pterodactyl": {"vanished": -75000000}
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "stegosaurus",
+ "prevName": null,
+ "data": {"vanished": -155000000}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "name": "",
+ "prevName": null,
+ "data": {"stegosaurus": {"vanished": -155000000}}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Limit to last one on ordered query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "vanished",
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "triceratops": {"vanished": -66000000},
+ "stegosaurus": {"vanished": -155000000},
+ "pterodactyl": {"vanished": -75000000}
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "triceratops",
+ "prevName": null,
+ "data": {"vanished": -66000000}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "name": "",
+ "prevName": null,
+ "data": {"triceratops": {"vanished": -66000000}}
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Update indexed value on existing child from limited query",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age",
+ "limitToLast": 4
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "4": { "age": 41, "highscore": 400, "name": "old mama"},
+ "5": { "age": 18, "highscore": 1200, "name": "young mama"},
+ "6": { "age": 20, "highscore": 1003, "name": "micheal blub"},
+ "7": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "4",
+ "prevName": "7",
+ "data": { "age": 41, "highscore": 400, "name": "old mama"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "5",
+ "prevName": null,
+ "data": { "age": 18, "highscore": 1200, "name": "young mama"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "6",
+ "prevName": "5",
+ "data": { "age": 20, "highscore": 1003, "name": "micheal blub"}
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "7",
+ "prevName": "6",
+ "data": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "4": { "age": 41, "highscore": 400, "name": "old mama"},
+ "5": { "age": 18, "highscore": 1200, "name": "young mama"},
+ "6": { "age": 20, "highscore": 1003, "name": "micheal blub"},
+ "7": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update the order by value, should cause a new value event",
+ "type": "serverUpdate",
+ "path": "4/age",
+ "tag": 1,
+ "data": 25,
+ "events": [
+ {
+ "path": "",
+ "type": "child_moved",
+ "name": "4",
+ "prevName": "6",
+ "data": { "age": 25, "highscore": 400, "name": "old mama"}
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "4",
+ "prevName": "6",
+ "data": { "age": 25, "highscore": 400, "name": "old mama"}
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "4": { "age": 25, "highscore": 400, "name": "old mama"},
+ "5": { "age": 18, "highscore": 1200, "name": "young mama"},
+ "6": { "age": 20, "highscore": 1003, "name": "micheal blub"},
+ "7": { "age": 30, "highscore": 10000, "name": "no. 7"}
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Can create startAt, endAt, equalTo queries with bool",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "boolKey",
+ "startAt": {"index": true}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 2,
+ "orderBy": "boolKey",
+ "endAt": {"index": true}
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 3,
+ "orderBy": "boolKey",
+ "equalTo": {"index": true}
+ },
+ "events": []
+ },
+ {
+ "type": "suppressWarning",
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Query with existing server snap",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age"
+ },
+ "events": []
+ },
+ {
+ ".comment": "untagged update, since index doesn't exist",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": { "age": 10, "score": 100, "bar": "baz" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": { "age": 10, "score": 100, "bar": "baz" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": { "age": 10, "score": 100, "bar": "baz" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "new listen should use existing data and index correctly",
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 2,
+ "orderBy": "score"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": { "age": 10, "score": 100, "bar": "baz" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": { "age": 10, "score": 100, "bar": "baz" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Server data is not purged for non-server-indexed queries",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "highscore",
+ "limitToLast": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "server has no index, so it sends down everything",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "highscore": 100, "value": "a" },
+ "b": { "highscore": 200, "value": "b" },
+ "c": { "highscore": 0, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "highscore": 100, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "highscore": 200, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "highscore": 100, "value": "a" },
+ "b": { "highscore": 200, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update of highscore leads to only a partial update",
+ "type": "serverUpdate",
+ "path": "c/highscore",
+ "data": 300,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "prevName": null,
+ "data": { "highscore": 100, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "highscore": 300, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "highscore": 200, "value": "b" },
+ "c": { "highscore": 300, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Limit with custom orderBy is refilled with correct item",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age",
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "age": 4 },
+ "b": { "age": 3 },
+ "c": { "age": 2 },
+ "d": { "age": 1 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "age": 4 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "age": 4 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "delete 'a' and make sure 'b' comes into view.",
+ "type": "set",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "age": 4 }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": { "age": 3 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "age": 3 }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "startAt/endAt dominates limit",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 2 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "server has no index, so it sends down everything",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 1000, "value": "b" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to fill limit and beyond",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "b": { "index": 1, "value": "b" },
+ "c": { "index": 2, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 1, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 1, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move entry out of window",
+ "type": "serverUpdate",
+ "path": "a/index",
+ "data": 1000,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 2, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 1, "value": "b" },
+ "c": { "index": 2, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move all but one entry out of window",
+ "type": "serverUpdate",
+ "path": "b/index",
+ "data": 1000,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 1, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "c": { "index": 2, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Update to single child that moves out of window",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move child out of query",
+ "type": "serverUpdate",
+ "path": "a/index",
+ "data": -1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move child out of query",
+ "type": "serverUpdate",
+ "path": "b/index",
+ "data": -1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Limited query doesn't pull in out of range child",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 1000, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server to move child out of query",
+ "type": "serverUpdate",
+ "path": "a/index",
+ "data": -1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Merge for location with default and limited listener",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "complete update",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 4, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "c",
+ "data": { "index": 4, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 4, "value": "d" }
+ }
+ }
+ ]
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 2
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "update from server pulls in other node",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "a": null,
+ "d": null
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "d",
+ "data": { "index": 4, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "User merge pulls in correct values",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 3
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 1000, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user merge pulls in existing value",
+ "type": "update",
+ "path": "d",
+ "data": { "index": 2 },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "b",
+ "data": { "index": 2, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "d": { "index": 2, "value": "d" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "User deep set pulls in correct values",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 3
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" },
+ "d": { "index": 1000, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 2, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "c": { "index": 3, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user deep set pulls in existing value",
+ "type": "set",
+ "path": "d/index",
+ "data": 2,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "index": 3, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": "b",
+ "data": { "index": 2, "value": "d" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 2, "value": "b" },
+ "d": { "index": 2, "value": "d" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Queries with equalTo(null) work",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": null },
+ "endAt": { "index": null }
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends all data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" },
+ "c": { "value": "c", "index": 1 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server updates existing value (bringing c into query)",
+ "type": "serverUpdate",
+ "path": "c/index",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" },
+ "c": { "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server updates existing value (sending c out of query)",
+ "type": "serverUpdate",
+ "path": "c/index",
+ "data": 1,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "value": "a" },
+ "b": { "value": "b" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Reverted writes update query",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends only query data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" },
+ "d": { "index": 6, "value": "d" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user adds new value should update query",
+ "type": "set",
+ "path": "",
+ "data": {
+ "c": { "index": 2, "value": "c" },
+ "a": { "index": 1, "value": "a" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "a",
+ "data": { "index": 2, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "c": { "index": 2, "value": "c" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "write is reverted should revert query to old state",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "c",
+ "data": { "index": 2, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Deep set for non-local data doesn't raise events",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "startAt": { "index": 1 },
+ "endAt": { "index": 10 },
+ "limitToFirst": 2
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends only query data",
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "index": 1, "value": "a" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "b": { "index": 5, "value": "b" }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user updates a value for node outside of query, should trigger no events",
+ "type": "set",
+ "path": "c/index",
+ "data": 1,
+ "events": [ ]
+ },
+ {
+ ".comment": "update from server now contains complete data",
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "c": { "index": 1, "value": "c" }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "b",
+ "data": { "index": 5, "value": "b" }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "a",
+ "data": { "index": 1, "value": "c" }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": { "index": 1, "value": "a" },
+ "c": { "index": 1, "value": "c" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "User update with new children triggers events",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "orderBy": "value",
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "update from server sends query data",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "value": 5 },
+ "c": { "value": 3 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": null,
+ "data": { "value": 3 }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": "c",
+ "data": { "value": 5 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "c": { "value": 3 },
+ "a": { "value": 5 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user adds new children through an update",
+ "type": "update",
+ "path": "",
+ "data": {
+ "b": { "value": 4 },
+ "d": { "value": 2 }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "d",
+ "prevName": null,
+ "data": { "value": 2 }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "c",
+ "data": { "value": 4 }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "d": { "value": 2 },
+ "c": { "value": 3 },
+ "b": { "value": 4 },
+ "a": { "value": 5 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server send new server",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "b": { "value": 4 },
+ "d": { "value": 2 }
+ },
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": [ ]
+ }
+ ]
+ },
+ {
+ "name": "User write with deep user overwrite",
+ "steps":
+ [
+ {
+ "type": "listen",
+ "path": "/foo",
+ "params": {
+ "orderBy": "value",
+ "tag": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "user sets initial data",
+ "type": "set",
+ "path": "/foo",
+ "data": {
+ "a": { "value": 1 },
+ "b": { "value": 5 },
+ "c": { "value": 10 }
+ },
+ "events": [
+ {
+ "path": "/foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": { "value": 1 }
+ },
+ {
+ "path": "/foo",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": { "value": 5 }
+ },
+ {
+ "path": "/foo",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": { "value": 10 }
+ },
+ {
+ "path": "/foo",
+ "type": "value",
+ "data": {
+ "a": { "value": 1 },
+ "b": { "value": 5 },
+ "c": { "value": 10 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user quickly overwrites value",
+ "type": "set",
+ "path": "/foo/c/value",
+ "data": 3,
+ "events": [
+ {
+ "path": "/foo",
+ "type": "child_moved",
+ "name": "c",
+ "prevName": "a",
+ "data": { "value": 3 }
+ },
+ {
+ "path": "/foo",
+ "type": "child_changed",
+ "name": "c",
+ "prevName": "a",
+ "data": { "value": 3 }
+ },
+ {
+ "path": "/foo",
+ "type": "value",
+ "data": {
+ "a": { "value": 1 },
+ "c": { "value": 3 },
+ "b": { "value": 5 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server sends complete but outdated data",
+ "type": "serverUpdate",
+ "path": "/foo",
+ "data": {
+ "a": { "value": 1 },
+ "b": { "value": 5 },
+ "c": { "value": 10 }
+ },
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [ ]
+ },
+ {
+ ".comment": "server sends update",
+ "type": "serverUpdate",
+ "path": "/foo/c/value",
+ "data": 3,
+ "events": [ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 1,
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Deep server merge",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": {
+ "bar1" : { "a": "baz1", "b": "qux1" },
+ "bar2" : { "a": "baz2", "b": "qux2" }
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "bar1",
+ "prevName": null,
+ "data": { "a": "baz1", "b": "qux1" }
+ },
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "bar2",
+ "prevName": "bar1",
+ "data": { "a": "baz2", "b": "qux2" }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "bar1" : { "a": "baz1", "b": "qux1" },
+ "bar2" : { "a": "baz2", "b": "qux2" }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "foo",
+ "data": {
+ "bar1/a": "newbaz1",
+ "bar2/b": "newqux2"
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "bar1",
+ "prevName": null,
+ "data": { "a": "newbaz1", "b": "qux1" }
+ },
+ {
+ "path": "foo",
+ "type": "child_changed",
+ "name": "bar2",
+ "prevName": "bar1",
+ "data": { "a": "baz2", "b": "newqux2" }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "bar1" : { "a": "newbaz1", "b": "qux1" },
+ "bar2" : { "a": "baz2", "b": "newqux2" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server updates priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "a/foo",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "data": { "foo": "bar" },
+ "events": [
+ {
+ "path": "a/foo",
+ "type": "value",
+ "data": "bar"
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": { "foo": "bar" }
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/foo/.priority",
+ "data": "qux",
+ "events": [
+ {
+ "path": "a/foo",
+ "type": "value",
+ "data": { ".value": "bar", ".priority": "qux" }
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "foo",
+ "prevName": null,
+ "data": { ".value": "bar", ".priority": "qux" }
+ },
+ {
+ "path": "a",
+ "type": "child_moved",
+ "name": "foo",
+ "prevName": null,
+ "data": { ".value": "bar", ".priority": "qux" }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "foo": { ".value": "bar", ".priority": "qux" }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert underlying full overwrite",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-a": "val-a",
+ "key-b": "val-b"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-a",
+ "prevName": null,
+ "data": "val-a"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-b",
+ "prevName": "key-a",
+ "data": "val-b"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-a": "val-a",
+ "key-b": "val-b"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "key-c": "val-c",
+ "key-d": "val-d"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-a",
+ "data": "val-a"
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-b",
+ "data": "val-b"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-c",
+ "prevName": null,
+ "data": "val-c"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-d",
+ "prevName": "key-c",
+ "data": "val-d"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-c": "val-c",
+ "key-d": "val-d"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "key-e": "val-e",
+ "key-f": "val-f"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-c",
+ "data": "val-c"
+ },
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-d",
+ "data": "val-d"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-e",
+ "prevName": null,
+ "data": "val-e"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-f",
+ "prevName": "key-e",
+ "data": "val-f"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-e": "val-e",
+ "key-f": "val-f"
+ }
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User child overwrite for non-existent server node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "foo",
+ "data": { "bar": "qux" },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": { "bar": "qux" }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert user overwrite of child on leaf node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": "foo",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "key",
+ "data": "value",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key",
+ "prevName": null,
+ "data": "value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": { "key": "value" }
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key",
+ "prevName": null,
+ "data": "value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server overwrite with deep user delete",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": "value-1"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": "value-1"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": "value-1"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User deletes non-existent key, which shouldn't trigger events",
+ "type": "set",
+ "path": "key-2/non-key",
+ "data": null,
+ "events": []
+ },
+ {
+ ".comment": "Server updates node with deep user delete",
+ "type": "serverUpdate",
+ "path": "key-2",
+ "data": {
+ "deep-key": "deep-value"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": {
+ "deep-key": "deep-value"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": "value-1",
+ "key-2": {
+ "deep-key": "deep-value"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User overwrites leaf node with priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Overwrite leaf with children node",
+ "type": "set",
+ "path": "foo",
+ "data": "bar",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User overwrites inherit priority values from leaf nodes",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user updates the node",
+ "type": "set",
+ "path": "foo",
+ "data": "foo-value",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "foo-value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "foo-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "The server updates the data for the set",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "Add another update, should not have old priority",
+ "type": "set",
+ "path": "bar",
+ "data": "bar-value",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "bar",
+ "prevName": null,
+ "data": "bar-value"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "foo-value",
+ "bar": "bar-value"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User update on user set leaf node with priority after server update",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "value",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user overwrite shadows server data",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [ ]
+ },
+ {
+ ".comment": "user updates the node",
+ "type": "update",
+ "path": "deep/deeper",
+ "data": {
+ "0-key": null,
+ "key": "value"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "deep",
+ "prevName": null,
+ "data": { "deeper": { "key": "value" } }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "deep": { "deeper": { "key": "value" } }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server deep delete on leaf node",
+ ".comment": "This is a contrived example, as the server will probably not send null updates to leaf nodes",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": "foo",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ },
+ {
+ ".comment": "this should trigger no events",
+ "type": "serverUpdate",
+ "path": "deep/child",
+ "data": null,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "User sets root priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": ".priority",
+ "data": "prio",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User updates priority on empty root",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ ".comment": "Priority on empty root should not trigger events",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio",
+ "events": []
+ },
+ {
+ ".comment": "This should a value event without priority",
+ "type": "serverUpdate",
+ "path": "",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ ".comment": "This should now have the user priority",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Revert set at root with priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites root",
+ "type": "set",
+ "path": "",
+ "data": {
+ "baz": "qux",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "baz",
+ "prevName": null,
+ "data": "qux"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".priority": "prio",
+ "baz": "qux"
+ }
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "baz",
+ "prevName": null,
+ "data": "qux"
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server updates priority after user sets priority",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": { ".value": "foo", ".priority": "prio" },
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": { ".value": "foo", ".priority": "prio" }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites priority",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio-2",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": { ".value": "foo", ".priority": "prio-2" }
+ }
+ ]
+ },
+ {
+ ".comment": "this should not trigger any events since a user write is shadowing",
+ "type": "serverUpdate",
+ "path": ".priority",
+ "data": null,
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User updates priority twice, first is reverted",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": { "foo": "bar" },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": { "foo": "bar" }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites priority first time",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio-1",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ ".priority": "prio-1"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites priority second time",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio-2",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ ".priority": "prio-2"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "revert should not trigger event",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": true,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "foo",
+ "data": "new-bar",
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "foo",
+ "prevName": null,
+ "data": "new-bar"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "new-bar",
+ ".priority": "prio-2"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Server acks root priority set after user deletes root node",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": "foo",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": "foo"
+ }
+ ]
+ },
+ {
+ ".comment": "User overwrites root priority",
+ "type": "set",
+ "path": ".priority",
+ "data": "prio",
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ ".value": "foo",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User deletes root node",
+ "type": "set",
+ "path": "",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": ".priority",
+ "data": "prio",
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "A delete in a merge doesn't push out nodes",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 3,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-3": 3,
+ "key-4": 4
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-1",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-4",
+ "prevName": "key-3",
+ "data": 4
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-3": 3,
+ "key-4": 4
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Since key-3 is deleted, key-5 should still remain in the query",
+ "type": "serverMerge",
+ "path": "",
+ "data": {
+ "key-3": null,
+ "key-2": 2
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-3",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-4": 4
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "A tagged query fires events eventually",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToLast": 2
+ },
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-3": 3
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": null,
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-2",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-2": 2,
+ "key-3": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server updates tagged data, should filter key-1 node",
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "key-2": 2,
+ "key-3": 3
+ },
+ "events": []
+ },
+ {
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "User deletes element, only child removed event is fired, since data is not available",
+ "type": "set",
+ "path": "key-2",
+ "data": null,
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-2",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-3": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server updates tagged data, should filter key-1 node",
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "key-1": 1,
+ "key-2": null
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "value",
+ "name": "",
+ "data": {
+ "key-1": 1,
+ "key-3": 3
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "A server update that leaves user sets unchanged is not ignored",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-2": 2
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": 1
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": 2
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user adds a new node",
+ "type": "set",
+ "path": "key-3",
+ "data": 3,
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-2",
+ "data": 3
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-3": 3
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server adds new children with full overwrite",
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-4": 4
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-4",
+ "prevName": "key-3",
+ "data": 4
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": 1,
+ "key-2": 2,
+ "key-3": 3,
+ "key-4": 4
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "User write outside of limit is ignored for tagged queries",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 2,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ },
+ "key-4": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-4",
+ "prevName": "key-1",
+ "data": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ },
+ "key-4": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "user updates index of child outside, which should bring it in view eventually, but not before the server sends the complete node",
+ "type": "set",
+ "path": "key-2/index",
+ "data": 2,
+ "events": []
+ },
+ {
+ ".comment": "In the meantime the server adds another node",
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ },
+ "key-3": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-4",
+ "data": {
+ "index": 4,
+ "other-key": "bar"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-3",
+ "prevName": "key-1",
+ "data": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ },
+ "key-3": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server now incorperates user update",
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "key-2",
+ "data": {
+ "index": 2,
+ "other-key": "qux"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "key-3",
+ "data": {
+ "index": 3,
+ "other-key": "baz"
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-2",
+ "prevName": "key-1",
+ "data": {
+ "index": 2,
+ "other-key": "qux"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ },
+ "key-2": {
+ "index": 2,
+ "other-key": "qux"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Ack for merge doesn't raise value event for later listen",
+ "steps": [
+ {
+ "type": "update",
+ "path": "",
+ "data": {
+ "foo": "bar"
+ },
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "",
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": "bar"
+ }
+ ]
+ },
+ {
+ "type": "ackUserWrite",
+ ".comment": "This acks a merge, so we can't raise a value event yet",
+ "writeId": 0,
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "foo": "bar",
+ "qux": "quux"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "qux",
+ "prevName": "foo",
+ "data": "quux"
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "foo": "bar",
+ "qux": "quux"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Clear parent shadowing server values merge with server children",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 2,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 2
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "a",
+ "data": {"b": 28, "c": 3},
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 28
+ }
+ ]
+ },
+ {
+ ".comment": "This listen should get a complete event snap, as well as complete server children",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": 28
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": 3
+ }
+ ]
+ },
+ {
+ ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": 29,
+ "events": []
+ },
+ {
+ ".comment": "Clearing the set should result in updated values for b",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": 29
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": 29
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Priorities don't make me sick",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "set",
+ "path": "a/foo",
+ "data": {
+ "bar": "baz",
+ ".priority": "prio"
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "foo",
+ "prevName": null,
+ "data": {
+ "bar": "baz",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "a/foo/bar",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "foo",
+ "data": {
+ "bar": "baz",
+ ".priority": "prio"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "this caused vomitting in the past...",
+ "type": "set",
+ "path": "a/foo/bar",
+ "data": null,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Merge that moves child from window to boundary does not cause child to be readded",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": [],
+ "params": {
+ "tag": 1,
+ "limitToFirst": 2,
+ "startAt": {"index": 1},
+ "orderBy": "index"
+ }
+ },
+ {
+ "type": "serverUpdate",
+ "path": "a",
+ "tag": 1,
+ "data": {
+ "2-a": {
+ "index": 10
+ },
+ "1-b": {
+ "index": 20
+ }
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "2-a",
+ "prevName": null,
+ "data": {
+ "index": 10
+ }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "1-b",
+ "prevName": "2-a",
+ "data": {
+ "index": 20
+ }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "2-a": { "index": 10 },
+ "1-b": { "index": 20 }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "2-a will be the 'next' child after the old '1-b' which will be updated first, but it shouldn't be added because it will actually be out of the window...",
+ "type": "update",
+ "path": "a",
+ "data": {
+ "1-b": { "index": 0 },
+ "2-a": { "index": 30 },
+ "3-c": { "index": 5 },
+ "4-d": { "index": 6 }
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "2-a",
+ "data": { "index": 10 }
+ },
+ {
+ "path": "a",
+ "type": "child_removed",
+ "name": "1-b",
+ "data": { "index": 20 }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "3-c",
+ "prevName": null,
+ "data": { "index": 5 }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "4-d",
+ "prevName": "3-c",
+ "data": { "index": 6 }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "3-c": { "index": 5 },
+ "4-d": { "index": 6 }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Deep merge ack is handled correctly.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "a",
+ "data": null,
+ "events": [
+ {
+ "path": "a",
+ "type": "value",
+ "data": null
+ }
+ ]
+ },
+ {
+ ".comment": "Do deep merge.",
+ "type": "update",
+ "path": "a/b",
+ "data": {
+ "c": 42,
+ "d": "hi"
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": 42,
+ "d": "hi"
+ }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "b": {
+ "c": 42,
+ "d": "hi"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server update for our deep merge.",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": {
+ "c": 42,
+ "d": "hi"
+ },
+ "events": []
+ },
+ {
+ ".comment": "ack deep merge.",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Deep merge ack (on incomplete data, and with server values)",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a/b",
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": {
+ "c": "original-server-value"
+ },
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_added",
+ "name": "c",
+ "data": "original-server-value",
+ "prevName": null
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {
+ "c": "original-server-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Do deep merge.",
+ "type": "update",
+ "path": "a/b",
+ "data": {
+ "c": "user-merge-value"
+ },
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_changed",
+ "name": "c",
+ "data": "user-merge-value",
+ "prevName": null
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {
+ "c": "user-merge-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Listen on a (which won't have complete data).",
+ "type": "listen",
+ "path": "a",
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": "user-merge-value"
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server update for our deep merge, but change data (simulate server value).",
+ "type": "serverUpdate",
+ "path": "a/b",
+ "data": {
+ "c": "user-merge-value-after-server-resolution"
+ },
+ "events": []
+ },
+ {
+ ".comment": "ack deep merge.",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "events": [
+ {
+ "path": "a/b",
+ "type": "child_changed",
+ "name": "c",
+ "data": "user-merge-value-after-server-resolution",
+ "prevName": null
+ },
+ {
+ "path": "a/b",
+ "type": "value",
+ "data": {
+ "c": "user-merge-value-after-server-resolution"
+ }
+ },
+ {
+ "path": "a",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "c": "user-merge-value-after-server-resolution"
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit query handles deep server merge for out-of-view item.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server merge for out-of-view child 'b' (perhaps for another listener). Shouldn't trigger events since we don't have complete data.",
+ "type": "serverMerge",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val"
+ },
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit query handles deep user merge for out-of-view item.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User merge for out-of-view child 'b'. Shouldn't trigger events since we don't have complete data.",
+ "type": "update",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val"
+ },
+ "events": [ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Limit query handles deep user merge for out-of-view item followed by server update.",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "foo",
+ "params": {
+ "tag": 1,
+ "limitToFirst": 1
+ },
+ "events": []
+ },
+ {
+ ".comment": "Initial server data.",
+ "type": "serverUpdate",
+ "path": "foo",
+ "tag": 1,
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ "events": [
+ {
+ "path": "foo",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "path": "foo",
+ "type": "value",
+ "data": {
+ "a": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "User merge for out-of-view child 'b'. Shouldn't trigger events since we don't have complete data.",
+ "type": "update",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val-new"
+ },
+ "events": [ ]
+ },
+ {
+ ".comment": "Server update for 'b', bringing it into view.",
+ "type": "serverUpdate",
+ "path": "foo/b",
+ "data": {
+ "val": "b-val-old",
+ "val2": "b-val2"
+ },
+ "events": [
+ {
+ "type": "child_removed",
+ "path": "foo",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "val": "a-val",
+ ".priority": "a-pri"
+ }
+ },
+ {
+ "type": "child_added",
+ "path": "foo",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "val": "b-val-new",
+ "val2": "b-val2"
+ }
+ },
+ {
+ "type": "value",
+ "path": "foo",
+ "data": {
+ "b": {
+ "val": "b-val-new",
+ "val2": "b-val2"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated, untagged update is not cached in tagged listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 1,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "server sends update for key-2 which should not be cached or marked complete",
+ "type": "serverUpdate",
+ "path": "key-2",
+ "data": {
+ "index": 2,
+ "other-key": "bar"
+ },
+ "events": []
+ },
+ {
+ ".comment": "Now an update for key-1 comes in, marking query as filtered",
+ "type": "serverMerge",
+ "tag": 1,
+ "path": "key-1",
+ "data": {
+ "other-key": "new-foo"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Server now updates node out of view, should not mark view unfiltered",
+ "type": "serverUpdate",
+ "path": "key-3",
+ "data": { "index": 3, "other-key": "qux" },
+ "events": []
+ },
+ {
+ ".comment": "Server now updates node out of view, should not raise any events",
+ "type": "serverMerge",
+ "path": "key-2",
+ "data": { "index": 0 },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated, acked set is not cached in tagged listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 1,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "set",
+ "path": "key-1/other-key",
+ "data": "new-foo",
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverUpdate",
+ "path": "key-1/other-key",
+ "tag": 1,
+ "data": "new-foo",
+ "events": []
+ },
+ {
+ ".comment": "The ack should not mark key-2 complete in tagged listen",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "Server now updates node out of view, should not raise any events",
+ "type": "serverMerge",
+ "path": "key-2",
+ "data": { "index": 0 },
+ "events": []
+ }
+ ]
+ },
+
+ {
+ "name": "Unrelated, acked update is not cached in tagged listen",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "index",
+ "limitToFirst": 1,
+ "startAt": {"index": null}
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "tag": 1,
+ "path": "",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "key-1",
+ "data": {
+ "other-key": "new-foo"
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "key-1",
+ "prevName": null,
+ "data": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "key-1": {
+ "index": 1,
+ "other-key": "new-foo"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "key-1",
+ "tag": 1,
+ "data": {
+ "other-key": "new-foo"
+ },
+ "events": []
+ },
+ {
+ ".comment": "The ack should not mark key-2 complete in tagged listen",
+ "type": "ackUserWrite",
+ "writeId": 0,
+ "revert": false,
+ "events": []
+ },
+ {
+ ".comment": "Server now updates node out of view, should not raise any events",
+ "type": "serverMerge",
+ "path": "key-2",
+ "data": { "index": 0 },
+ "events": []
+ }
+ ]
+ },
+ {
+ "name": "Deep update raises immediate events only if has complete data",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "age",
+ "limitToLast": 1
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a": {
+ "age": 4
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": {
+ "age": 4
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "",
+ "data": {
+ "a/age": 0,
+ "e": {
+ "age": 4
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "e",
+ "prevName": null,
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "e": {
+ "age": 4
+ }
+ }
+ }
+ ]
+ },
+ {
+ ".comment": "Now we don't have a full data for child /f, don't raise the event. The events for child /e are correct, although may be confusing for customers.",
+ "type": "update",
+ "path": "",
+ "data": {
+ "e/age": 0,
+ "f/age": 4
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_moved",
+ "name": "e",
+ "prevName": null,
+ "data": {
+ "age": 0
+ }
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "e",
+ "prevName": null,
+ "data": {
+ "age": 0
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "e": {
+ "age": 0
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "f": {
+ "age": 4
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "e",
+ "data": {
+ "age": 0
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "f",
+ "prevName": null,
+ "data": {
+ "age": 4
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "f": {
+ "age": 4
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Deep update returns minimum data required",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "",
+ "params": {
+ "tag": 1,
+ "orderBy": "idx",
+ "equalTo": { "index": true }
+ },
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a": {
+ "name": "foo",
+ "idx": true
+ },
+ "b": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "a",
+ "prevName": null,
+ "data": {
+ "name": "foo",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "b",
+ "prevName": "a",
+ "data": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "a": {
+ "name": "foo",
+ "idx": true
+ },
+ "b": {
+ "name": "bar",
+ "idx": true
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "",
+ "tag": 1,
+ "data": {
+ "a/idx": false,
+ "b/name": "blah",
+ "c": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ "events": [
+ {
+ "path": "",
+ "type": "child_removed",
+ "name": "a",
+ "data": {
+ "name": "foo",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "child_changed",
+ "name": "b",
+ "prevName": null,
+ "data": {
+ "name": "blah",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "child_added",
+ "name": "c",
+ "prevName": "b",
+ "data": {
+ "name": "bar",
+ "idx": true
+ }
+ },
+ {
+ "path": "",
+ "type": "value",
+ "data": {
+ "b": {
+ "name": "blah",
+ "idx": true
+ },
+ "c": {
+ "name": "bar",
+ "idx": true
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Deep update raises all events",
+ "steps": [
+ {
+ "type": "listen",
+ "path": "a",
+ "events": []
+ },
+ {
+ "type": "listen",
+ "path": "b",
+ "events": []
+ },
+ {
+ "type": "serverUpdate",
+ "path": "",
+ "data": {
+ "a": { "aa": 1, "ab": 2 },
+ "b": { "ba": 3, "bb": 4 }
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_added",
+ "prevName": null,
+ "name": "aa",
+ "data": 1
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "prevName": "aa",
+ "name": "ab",
+ "data": 2
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 1,
+ "ab": 2
+ }
+ },
+ {
+ "path": "b",
+ "type": "child_added",
+ "prevName": null,
+ "name": "ba",
+ "data": 3
+ },
+ {
+ "path": "b",
+ "type": "child_added",
+ "prevName": "ba",
+ "name": "bb",
+ "data": 4
+ },
+ {
+ "path": "b",
+ "type": "value",
+ "data": {
+ "ba": 3,
+ "bb": 4
+ }
+ }
+ ]
+ },
+ {
+ "type": "update",
+ "path": "",
+ "data": {
+ "a/aa": 0,
+ "b/ba": 0
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "prevName": null,
+ "name": "aa",
+ "data": 0
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 0,
+ "ab": 2
+ }
+ },
+ {
+ "path": "b",
+ "type": "child_changed",
+ "prevName": null,
+ "name": "ba",
+ "data": 0
+ },
+ {
+ "path": "b",
+ "type": "value",
+ "data": {
+ "ba": 0,
+ "bb": 4
+ }
+ }
+ ]
+ },
+ {
+ "type": "serverMerge",
+ "path": "a",
+ "data": {
+ "ab/abc": 1,
+ "ac/acd": 2
+ },
+ "events": [
+ {
+ "path": "a",
+ "type": "child_changed",
+ "prevName": "aa",
+ "name": "ab",
+ "data": { "abc": 1 }
+ },
+ {
+ "path": "a",
+ "type": "child_added",
+ "prevName": "ab",
+ "name": "ac",
+ "data": { "acd": 2 }
+ },
+ {
+ "path": "a",
+ "type": "value",
+ "data": {
+ "aa": 0,
+ "ab": { "abc": 1 },
+ "ac": { "acd": 2 }
+ }
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/Example/Database/Tests/third_party/Base64.h b/Example/Database/Tests/third_party/Base64.h
new file mode 100644
index 0000000..6db1028
--- /dev/null
+++ b/Example/Database/Tests/third_party/Base64.h
@@ -0,0 +1,53 @@
+//
+// Base64.h
+//
+// Version 1.1
+//
+// Created by Nick Lockwood on 12/01/2012.
+// Copyright (C) 2012 Charcoal Design
+//
+// Distributed under the permissive zlib License
+// Get the latest version from here:
+//
+// https://github.com/nicklockwood/Base64
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#import <Foundation/Foundation.h>
+
+
+@interface NSData (Base64)
+
++ (NSData *)dataWithBase64EncodedString:(NSString *)string;
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth;
+- (NSString *)base64EncodedString;
+
+@end
+
+
+@interface NSString (Base64)
+
++ (NSString *)stringWithBase64EncodedString:(NSString *)string;
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth;
+- (NSString *)base64EncodedString;
+- (NSString *)base64DecodedString;
+- (NSData *)base64DecodedData;
+
+@end
diff --git a/Example/Database/Tests/third_party/Base64.m b/Example/Database/Tests/third_party/Base64.m
new file mode 100644
index 0000000..b3d73db
--- /dev/null
+++ b/Example/Database/Tests/third_party/Base64.m
@@ -0,0 +1,202 @@
+//
+// Base64.m
+//
+// Version 1.1
+//
+// Created by Nick Lockwood on 12/01/2012.
+// Copyright (C) 2012 Charcoal Design
+//
+// Distributed under the permissive zlib License
+// Get the latest version from here:
+//
+// https://github.com/nicklockwood/Base64
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+// claim that you wrote the original software. If you use this software
+// in a product, an acknowledgment in the product documentation would be
+// appreciated but is not required.
+//
+// 2. Altered source versions must be plainly marked as such, and must not be
+// misrepresented as being the original software.
+//
+// 3. This notice may not be removed or altered from any source distribution.
+//
+
+#import "Base64.h"
+
+
+#import <Availability.h>
+#if !__has_feature(objc_arc)
+#error This library requires automatic reference counting
+#endif
+
+
+@implementation NSData (Base64)
+
++ (NSData *)dataWithBase64EncodedString:(NSString *)string
+{
+ const char lookup[] =
+ {
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99,
+ 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99,
+ 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99
+ };
+
+ NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
+ long long inputLength = [inputData length];
+ const unsigned char *inputBytes = [inputData bytes];
+
+ long long maxOutputLength = (inputLength / 4 + 1) * 3;
+ NSMutableData *outputData = [NSMutableData dataWithLength:maxOutputLength];
+ unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes];
+
+ int accumulator = 0;
+ long long outputLength = 0;
+ unsigned char accumulated[] = {0, 0, 0, 0};
+ for (long long i = 0; i < inputLength; i++)
+ {
+ unsigned char decoded = lookup[inputBytes[i] & 0x7F];
+ if (decoded != 99)
+ {
+ accumulated[accumulator] = decoded;
+ if (accumulator == 3)
+ {
+ outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3];
+ }
+ accumulator = (accumulator + 1) % 4;
+ }
+ }
+
+ //handle left-over data
+ if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4);
+ if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2);
+ if (accumulator > 2) outputLength++;
+
+ //truncate data to match actual output length
+ outputData.length = outputLength;
+ return outputLength? outputData: nil;
+}
+
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth
+{
+ //ensure wrapWidth is a multiple of 4
+ wrapWidth = (wrapWidth / 4) * 4;
+
+ const char lookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ long long inputLength = [self length];
+ const unsigned char *inputBytes = [self bytes];
+
+ long long maxOutputLength = (inputLength / 3 + 1) * 4;
+ maxOutputLength += wrapWidth? (maxOutputLength / wrapWidth) * 2: 0;
+ unsigned char *outputBytes = (unsigned char *)malloc(maxOutputLength);
+
+ long long i;
+ long long outputLength = 0;
+ for (i = 0; i < inputLength - 2; i += 3)
+ {
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
+ outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
+ outputBytes[outputLength++] = lookup[((inputBytes[i + 1] & 0x0F) << 2) | ((inputBytes[i + 2] & 0xC0) >> 6)];
+ outputBytes[outputLength++] = lookup[inputBytes[i + 2] & 0x3F];
+
+ //add line break
+ if (wrapWidth && (outputLength + 2) % (wrapWidth + 2) == 0)
+ {
+ outputBytes[outputLength++] = '\r';
+ outputBytes[outputLength++] = '\n';
+ }
+ }
+
+ //handle left-over data
+ if (i == inputLength - 2)
+ {
+ // = terminator
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
+ outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)];
+ outputBytes[outputLength++] = lookup[(inputBytes[i + 1] & 0x0F) << 2];
+ outputBytes[outputLength++] = '=';
+ }
+ else if (i == inputLength - 1)
+ {
+ // == terminator
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2];
+ outputBytes[outputLength++] = lookup[(inputBytes[i] & 0x03) << 4];
+ outputBytes[outputLength++] = '=';
+ outputBytes[outputLength++] = '=';
+ }
+
+ if (outputLength >= 4)
+ {
+ //truncate data to match actual output length
+ outputBytes = realloc(outputBytes, outputLength);
+ return [[NSString alloc] initWithBytesNoCopy:outputBytes
+ length:outputLength
+ encoding:NSASCIIStringEncoding
+ freeWhenDone:YES];
+ }
+ else if (outputBytes)
+ {
+ free(outputBytes);
+ }
+ return nil;
+}
+
+- (NSString *)base64EncodedString
+{
+ return [self base64EncodedStringWithWrapWidth:0];
+}
+
+@end
+
+
+@implementation NSString (Base64)
+
++ (NSString *)stringWithBase64EncodedString:(NSString *)string
+{
+ NSData *data = [NSData dataWithBase64EncodedString:string];
+ if (data)
+ {
+ return [[self alloc] initWithData:data encoding:NSUTF8StringEncoding];
+ }
+ return nil;
+}
+
+- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth
+{
+ NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
+ return [data base64EncodedStringWithWrapWidth:wrapWidth];
+}
+
+- (NSString *)base64EncodedString
+{
+ NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
+ return [data base64EncodedString];
+}
+
+- (NSString *)base64DecodedString
+{
+ return [NSString stringWithBase64EncodedString:self];
+}
+
+- (NSData *)base64DecodedData
+{
+ return [NSData dataWithBase64EncodedString:self];
+}
+
+@end