aboutsummaryrefslogtreecommitdiffhomepage
path: root/Example/Database/Tests/Integration
diff options
context:
space:
mode:
Diffstat (limited to 'Example/Database/Tests/Integration')
-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
22 files changed, 10886 insertions, 0 deletions
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