aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Database/Tests
diff options
context:
space:
mode:
authorGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
committerGravatar Paul Beusterien <paulbeusterien@google.com>2017-05-15 12:27:07 -0700
commit98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch)
tree131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Example/Database/Tests
parent32461366c9e204a527ca05e6e9b9404a2454ac51 (diff)
Initial
Diffstat (limited to 'Example/Database/Tests')
-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
78 files changed, 26831 insertions, 0 deletions
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