diff options
Diffstat (limited to 'Example/Database/Tests/Integration')
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 |