/* * 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 [reader removeAllObservers]; [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