diff options
author | Paul Beusterien <paulbeusterien@google.com> | 2017-05-15 12:27:07 -0700 |
---|---|---|
committer | Paul Beusterien <paulbeusterien@google.com> | 2017-05-15 12:27:07 -0700 |
commit | 98ba64449a632518bd2b86fe8d927f4a960d3ddc (patch) | |
tree | 131d9c4272fa6179fcda6c5a33fcb3b1bd57ad2e /Example/Database/Tests | |
parent | 32461366c9e204a527ca05e6e9b9404a2454ac51 (diff) |
Initial
Diffstat (limited to 'Example/Database/Tests')
78 files changed, 26831 insertions, 0 deletions
diff --git a/Example/Database/Tests/FirebaseTests-Info.plist b/Example/Database/Tests/FirebaseTests-Info.plist new file mode 100644 index 0000000..42887ee --- /dev/null +++ b/Example/Database/Tests/FirebaseTests-Info.plist @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>com.firebase.mobile.ios.${PRODUCT_NAME:rfc1034identifier}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>BNDL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/Example/Database/Tests/Helpers/FDevice.h b/Example/Database/Tests/Helpers/FDevice.h new file mode 100644 index 0000000..c32aea0 --- /dev/null +++ b/Example/Database/Tests/Helpers/FDevice.h @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +@class FIRDatabaseReference; +@class SenTest; + +@interface FDevice : NSObject +- (id)initOnline; +- (id)initOffline; +- (id)initOnlineWithUrl:(NSString *)firebaseUrl; +- (id)initOfflineWithUrl:(NSString *)firebaseUrl; +- (void)goOffline; +- (void)goOnline; +- (void)restartOnline; +- (void)restartOffline; +- (void)waitForIdleUsingWaiter:(XCTest*)waiter; +- (void)do:(void (^)(FIRDatabaseReference *))action; + +- (void)dispose; + +@end diff --git a/Example/Database/Tests/Helpers/FDevice.m b/Example/Database/Tests/Helpers/FDevice.m new file mode 100644 index 0000000..f9667df --- /dev/null +++ b/Example/Database/Tests/Helpers/FDevice.m @@ -0,0 +1,133 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FDevice.h" +#import "FIRDatabaseReference.h" +#import "FRepoManager.h" +#import "FIRDatabaseReference_Private.h" +#import "FIRDatabaseConfig_Private.h" +#import "SenTest+FWaiter.h" +#import "FTestHelpers.h" + +@interface FDevice() { + FIRDatabaseConfig * config; + NSString *url; + BOOL isOnline; + BOOL disposed; +} +@end + +@implementation FDevice + +- (id)initOnline { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + return [self initOnlineWithUrl:[ref description]]; +} + +- (id)initOffline { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + return [self initOfflineWithUrl:[ref description]]; +} + +- (id)initOnlineWithUrl:(NSString *)firebaseUrl { + return [self initWithUrl:firebaseUrl andOnline:YES]; +} + +- (id)initOfflineWithUrl:(NSString *)firebaseUrl { + return [self initWithUrl:firebaseUrl andOnline:NO]; +} + +static NSUInteger deviceId = 0; + +- (id)initWithUrl:(NSString *)firebaseUrl andOnline:(BOOL)online { + self = [super init]; + if (self) { + config = [FIRDatabaseConfig configForName:[NSString stringWithFormat:@"device-%lu", deviceId++]]; + config.persistenceEnabled = YES; + url = firebaseUrl; + isOnline = online; + } + return self; +} + +- (void) dealloc +{ + if (!self->disposed) { + [NSException raise:NSInternalInconsistencyException format:@"Forgot to dispose device"]; + } +} + +- (void) dispose { + // TODO: clear persistence + [FRepoManager disposeRepos:self->config]; + self->disposed = YES; +} + +- (void)goOffline { + isOnline = NO; + [FRepoManager interrupt:config]; +} + +- (void)goOnline { + isOnline = YES; + [FRepoManager resume:config]; +} + +- (void)restartOnline { + @autoreleasepool { + [FRepoManager disposeRepos:config]; + isOnline = YES; + } +} + +- (void)restartOffline { + @autoreleasepool { + [FRepoManager disposeRepos:config]; + isOnline = NO; + } +} + +// Waits for us to connect and then does an extra round-trip to make sure all initial state restoration is completely done. +- (void)waitForIdleUsingWaiter:(XCTest*)waiter { + [self do:^(FIRDatabaseReference *ref) { + __block BOOL connected = NO; + FIRDatabaseHandle handle = [[ref.root child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + connected = [snapshot.value boolValue]; + }]; + [waiter waitUntil:^BOOL { return connected; }]; + [ref.root removeObserverWithHandle:handle]; + + // HACK: Do a deep setPriority (which we expect to fail because there's no data there) to do a no-op roundtrip. + __block BOOL done = NO; + [[ref.root child:@"ENTOHTNUHOE/ONTEHNUHTOE"] setPriority:@"blah" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + [waiter waitUntil:^BOOL { return done; }]; + }]; +} + +- (void)do:(void (^)(FIRDatabaseReference *))action { + @autoreleasepool { + FIRDatabaseReference *ref = [[[[FIRDatabaseReference alloc] initWithConfig:self->config] database] referenceFromURL:self->url]; + if (!isOnline) { + [FRepoManager interrupt:config]; + } + action(ref); + } +} + +@end diff --git a/Example/Database/Tests/Helpers/FEventTester.h b/Example/Database/Tests/Helpers/FEventTester.h new file mode 100644 index 0000000..b3503b9 --- /dev/null +++ b/Example/Database/Tests/Helpers/FEventTester.h @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> + +@interface FEventTester : XCTestCase + +- (id)initFrom:(XCTestCase *)elsewhere; +- (void) addLookingFor:(NSArray *)l; +- (void) wait; +- (void) waitForInitialization; +- (void) unregister; + +@property (nonatomic, strong) NSMutableArray* lookingFor; +@property (readwrite) int callbacksCalled; +@property (nonatomic, strong) NSMutableDictionary* seenFirebaseLocations; +//@property (nonatomic, strong) NSMutableDictionary* initializationEvents; +@property (nonatomic, strong) XCTestCase* from; +@property (nonatomic, strong) NSMutableArray* errors; +@property (nonatomic, strong) NSMutableArray* actualPathsAndEvents; +@property (nonatomic) int initializationEvents; + +@end diff --git a/Example/Database/Tests/Helpers/FEventTester.m b/Example/Database/Tests/Helpers/FEventTester.m new file mode 100644 index 0000000..fa7c081 --- /dev/null +++ b/Example/Database/Tests/Helpers/FEventTester.m @@ -0,0 +1,172 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FEventTester.h" +#import "FIRDatabaseReference.h" +#import "FTupleBoolBlock.h" +#import "FTupleEventTypeString.h" +#import "FTestHelpers.h" +#import "SenTest+FWaiter.h" + +@implementation FEventTester + +@synthesize lookingFor; +@synthesize callbacksCalled; +@synthesize from; +@synthesize errors; +@synthesize seenFirebaseLocations; +@synthesize initializationEvents; +@synthesize actualPathsAndEvents; + +- (id)initFrom:(XCTestCase *)elsewhere +{ + self = [super init]; + if (self) { + self.seenFirebaseLocations = [[NSMutableDictionary alloc] init]; + self.initializationEvents = 0; + self.lookingFor = [[NSMutableArray alloc] init]; + self.actualPathsAndEvents = [[NSMutableArray alloc] init]; + self.from = elsewhere; + self.callbacksCalled = 0; + } + return self; +} + +- (void) addLookingFor:(NSArray *)l { + + // expect them in the order they're given to us + [self.lookingFor addObjectsFromArray:l]; + + + // see notes on ordering of listens in init.spec.js + NSArray* toListen = [l sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { + FTupleEventTypeString* a = obj1; + FTupleEventTypeString* b = obj2; + NSUInteger lenA = [a.firebase description].length; + NSUInteger lenB = [b.firebase description].length; + if (lenA < lenB) { + return NSOrderedAscending; + } else if (lenA == lenB) { + return NSOrderedSame; + } else { + return NSOrderedDescending; + } + }]; + + for(FTupleEventTypeString* fevts in toListen) { + if(! [self.seenFirebaseLocations objectForKey:[fevts.firebase description]]) { + fevts.vvcallback = [self listenOnPath:fevts.firebase]; + fevts.initialized = NO; + [self.seenFirebaseLocations setObject:fevts forKey:[fevts.firebase description]]; + } + } +} + +- (void) unregister { + for(FTupleEventTypeString* fevts in self.lookingFor) { + if (fevts.vvcallback) { + fevts.vvcallback(); + } + } + [self.lookingFor removeAllObjects]; +} + +- (fbt_void_void) listenOnPath:(FIRDatabaseReference *)path { + FIRDatabaseHandle removedHandle = [path observeEventType:FIRDataEventTypeChildRemoved withBlock:[self makeEventCallback:FIRDataEventTypeChildRemoved]]; + FIRDatabaseHandle addedHandle = [path observeEventType:FIRDataEventTypeChildAdded withBlock:[self makeEventCallback:FIRDataEventTypeChildAdded]]; + FIRDatabaseHandle movedHandle = [path observeEventType:FIRDataEventTypeChildMoved withBlock:[self makeEventCallback:FIRDataEventTypeChildMoved]]; + FIRDatabaseHandle changedHandle = [path observeEventType:FIRDataEventTypeChildChanged withBlock:[self makeEventCallback:FIRDataEventTypeChildChanged]]; + FIRDatabaseHandle valueHandle = [path observeEventType:FIRDataEventTypeValue withBlock:[self makeEventCallback:FIRDataEventTypeValue]]; + + fbt_void_void cb = ^() { + [path removeObserverWithHandle:removedHandle]; + [path removeObserverWithHandle:addedHandle]; + [path removeObserverWithHandle:movedHandle]; + [path removeObserverWithHandle:changedHandle]; + [path removeObserverWithHandle:valueHandle]; + }; + return [cb copy]; +} + +- (void) wait { + [self waitUntil:^BOOL{ + return self.actualPathsAndEvents.count >= self.lookingFor.count; + } timeout:kFirebaseTestTimeout]; + + for (int i = 0; i < self.lookingFor.count; ++i) { + FTupleEventTypeString* target = [self.lookingFor objectAtIndex:i]; + FTupleEventTypeString* recvd = [self.actualPathsAndEvents objectAtIndex:i]; + XCTAssertTrue([target isEqualTo:recvd], @"Expected %@ to match %@", target, recvd); + } + + if (self.actualPathsAndEvents.count > self.lookingFor.count) { + NSLog(@"Too many events: %@", self.actualPathsAndEvents); + XCTFail(@"Received too many events"); + } +} + +- (void) waitForInitialization { + [self waitUntil:^BOOL{ + for (FTupleEventTypeString* evt in [self.seenFirebaseLocations allValues]) { + if (!evt.initialized) { + return NO; + } + } + + // splice out all of the initialization events + NSRange theRange; + theRange.location = 0; + theRange.length = self.initializationEvents; + [actualPathsAndEvents removeObjectsInRange:theRange]; + + return YES; + } timeout:kFirebaseTestTimeout]; +} + +- (fbt_void_datasnapshot) makeEventCallback:(FIRDataEventType)type { + + fbt_void_datasnapshot cb = ^(FIRDataSnapshot * snap) { + + FIRDatabaseReference * ref = snap.ref; + NSString* name = nil; + if (type != FIRDataEventTypeValue) { + ref = ref.parent; + name = snap.key; + } + + FTupleEventTypeString* evt = [[FTupleEventTypeString alloc] initWithFirebase:ref withEvent:type withString:name]; + [actualPathsAndEvents addObject:evt]; + + NSLog(@"Adding event: %@ (%@)", evt, [snap value]); + + FTupleEventTypeString* targetEvt = [self.seenFirebaseLocations objectForKey:[ref description]]; + if (targetEvt && !targetEvt.initialized) { + self.initializationEvents++; + if (type == FIRDataEventTypeValue) { + targetEvt.initialized = YES; + } + } + }; + return [cb copy]; +} + + +- (void) failWithException:(NSException *) anException { + //TODO: FIX + @throw anException; +} + +@end diff --git a/Example/Database/Tests/Helpers/FIRFakeApp.h b/Example/Database/Tests/Helpers/FIRFakeApp.h new file mode 100644 index 0000000..afe976a --- /dev/null +++ b/Example/Database/Tests/Helpers/FIRFakeApp.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +@class FIRFakeOptions; + +@interface FIRFakeApp : NSObject + +- (instancetype) initWithName:(NSString *)name URL:(NSString *)url; + +@property(nonatomic, readonly) FIRFakeOptions *options; +@property(nonatomic, copy, readonly) NSString *name; +@end diff --git a/Example/Database/Tests/Helpers/FIRFakeApp.m b/Example/Database/Tests/Helpers/FIRFakeApp.m new file mode 100644 index 0000000..b7abe81 --- /dev/null +++ b/Example/Database/Tests/Helpers/FIRFakeApp.m @@ -0,0 +1,48 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRFakeApp.h" + +@interface FIRFakeOptions: NSObject +@property(nonatomic, readonly, copy) NSString *databaseURL; +- (instancetype) initWithURL:(NSString *)url; +@end + +@implementation FIRFakeOptions +- (instancetype) initWithURL:(NSString *)url { + self = [super init]; + if (self) { + self->_databaseURL = url; + } + return self; +} +@end + +@implementation FIRFakeApp + +- (instancetype) initWithName:(NSString *)name URL:(NSString *)url { + self = [super init]; + if (self) { + self->_name = name; + self->_options = [[FIRFakeOptions alloc] initWithURL:url]; + } + return self; +} + +- (void)getTokenForcingRefresh:(BOOL)forceRefresh withCallback:(void (^)(NSString *_Nullable token, NSError *_Nullable error))callback { + callback(nil, nil); +} +@end diff --git a/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h new file mode 100644 index 0000000..e2a5751 --- /dev/null +++ b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.h @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import "FAuthTokenProvider.h" + +@interface FIRTestAuthTokenProvider : NSObject <FAuthTokenProvider> + +@property (nonatomic, strong) NSString *token; +@property (nonatomic, strong) NSString *nextToken; + +- (instancetype) initWithToken:(NSString *)token; +- (instancetype) init NS_UNAVAILABLE; + +@end diff --git a/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m new file mode 100644 index 0000000..4719295 --- /dev/null +++ b/Example/Database/Tests/Helpers/FIRTestAuthTokenProvider.m @@ -0,0 +1,61 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRTestAuthTokenProvider.h" +#import "FIRDatabaseQuery_Private.h" + +@interface FIRTestAuthTokenProvider () + +@property (nonatomic, strong) NSMutableArray *listeners; + +@end + +@implementation FIRTestAuthTokenProvider + +- (instancetype) initWithToken:(NSString *)token { + self = [super init]; + if (self != nil) { + self.listeners = [NSMutableArray array]; + self.token = token; + } + return self; +} + +- (void) setToken:(NSString *)token { + self->_token = token; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + [self.listeners enumerateObjectsUsingBlock:^(fbt_void_nsstring _Nonnull listener, NSUInteger idx, BOOL * _Nonnull stop) { + listener(token); + }]; + }); + +} + +- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback { + if (forceRefresh) { + self.token = self.nextToken; + } + // Simulate delay + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), [FIRDatabaseQuery sharedQueue], ^{ + callback(self.token, nil); + }); +} + +- (void) listenForTokenChanges:(fbt_void_nsstring)listener { + [self.listeners addObject:[listener copy]]; +} + +@end diff --git a/Example/Database/Tests/Helpers/FMockStorageEngine.h b/Example/Database/Tests/Helpers/FMockStorageEngine.h new file mode 100644 index 0000000..98a7d84 --- /dev/null +++ b/Example/Database/Tests/Helpers/FMockStorageEngine.h @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "FStorageEngine.h" + +@interface FMockStorageEngine : NSObject<FStorageEngine> + +@end diff --git a/Example/Database/Tests/Helpers/FMockStorageEngine.m b/Example/Database/Tests/Helpers/FMockStorageEngine.m new file mode 100644 index 0000000..98cb596 --- /dev/null +++ b/Example/Database/Tests/Helpers/FMockStorageEngine.m @@ -0,0 +1,168 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FMockStorageEngine.h" + +#import "FWriteRecord.h" +#import "FCompoundWrite.h" +#import "FNode.h" +#import "FEmptyNode.h" +#import "FTrackedQuery.h" +#import "FPruneForest.h" +#import "FCompoundWrite.h" + +@interface FMockStorageEngine () + +@property (nonatomic) BOOL closed; +@property (nonatomic, strong) NSMutableDictionary *userWritesDict; +@property (nonatomic, strong) FCompoundWrite *serverCache; +@property (nonatomic, strong) NSMutableDictionary *trackedQueries; +@property (nonatomic, strong) NSMutableDictionary *trackedQueryKeys; + +@end + +@implementation FMockStorageEngine + +- (id)init { + self = [super init]; + if (self != nil) { + self->_userWritesDict = [NSMutableDictionary dictionary]; + self->_serverCache = [FCompoundWrite emptyWrite]; + self->_trackedQueries = [NSMutableDictionary dictionary]; + self->_trackedQueryKeys = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)close { + self.closed = YES; +} + +- (void)saveUserOverwrite:(id<FNode>)node atPath:(FPath *)path writeId:(NSUInteger)writeId { + FWriteRecord *writeRecord = [[FWriteRecord alloc] initWithPath:path overwrite:node writeId:writeId visible:YES]; + self.userWritesDict[@(writeId)] = writeRecord; +} + +- (void)saveUserMerge:(FCompoundWrite *)merge atPath:(FPath *)path writeId:(NSUInteger)writeId { + FWriteRecord *writeRecord = [[FWriteRecord alloc] initWithPath:path merge:merge writeId:writeId]; + self.userWritesDict[@(writeId)] = writeRecord; +} + +- (void)removeUserWrite:(NSUInteger)writeId { + [self.userWritesDict removeObjectForKey:@(writeId)]; +} + +- (void)removeAllUserWrites { + [self.userWritesDict removeAllObjects]; +} + +- (NSArray *)userWrites { + return [[self.userWritesDict allValues] sortedArrayUsingComparator:^NSComparisonResult(FWriteRecord *obj1, FWriteRecord *obj2) { + if (obj1.writeId < obj2.writeId) { + return NSOrderedAscending; + } else if (obj1.writeId > obj2.writeId) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } + }]; +} + +- (id<FNode>)serverCacheAtPath:(FPath *)path { + return [[self.serverCache childCompoundWriteAtPath:path] applyToNode:[FEmptyNode emptyNode]]; +} + +- (id<FNode>)serverCacheForKeys:(NSSet *)keys atPath:(FPath *)path { + __block id<FNode> children = [FEmptyNode emptyNode]; + id<FNode> fullNode = [[self.serverCache childCompoundWriteAtPath:path] applyToNode:[FEmptyNode emptyNode]]; + [keys enumerateObjectsUsingBlock:^(NSString *key, BOOL *stop) { + children = [children updateImmediateChild:key withNewChild:[fullNode getImmediateChild:key]]; + }]; + return children; +} + +- (void)updateServerCache:(id<FNode>)node atPath:(FPath *)path merge:(BOOL)merge { + if (merge) { + [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> childNode, BOOL *stop) { + self.serverCache = [self.serverCache addWrite:childNode atPath:[path childFromString:key]]; + }]; + } else { + self.serverCache = [self.serverCache addWrite:node atPath:path]; + } +} + +- (void)updateServerCacheWithMerge:(FCompoundWrite *)merge atPath:(FPath *)path { + self.serverCache = [self.serverCache addCompoundWrite:merge atPath:path]; +} + +- (NSUInteger)serverCacheEstimatedSizeInBytes { + id data = [[self.serverCache applyToNode:[FEmptyNode emptyNode]] valForExport:YES]; + return [NSJSONSerialization dataWithJSONObject:data options:0 error:nil].length; +} + +- (void)pruneCache:(FPruneForest *)pruneForest atPath:(FPath *)prunePath { + [self.serverCache enumerateWrites:^(FPath *absolutePath, id<FNode> node, BOOL *stop) { + NSAssert([prunePath isEqual:absolutePath] || ![absolutePath contains:prunePath], @"Pruning at %@ but we found data higher up!", prunePath); + if ([prunePath contains:absolutePath]) { + FPath *relativePath = [FPath relativePathFrom:prunePath to:absolutePath]; + if ([pruneForest shouldPruneUnkeptDescendantsAtPath:relativePath]) { + __block FCompoundWrite *newCache = [FCompoundWrite emptyWrite]; + [[pruneForest childAtPath:relativePath] enumarateKeptNodesUsingBlock:^(FPath *keepPath) { + newCache = [newCache addWrite:[node getChild:keepPath] atPath:keepPath]; + }]; + self.serverCache = [[self.serverCache removeWriteAtPath:absolutePath] addCompoundWrite:newCache atPath:absolutePath]; + } else { + // NOTE: This is technically a valid scenario (e.g. you ask to prune at / but only want to prune + // 'foo' and 'bar' and ignore everything else). But currently our pruning will explicitly + // prune or keep everything we know about, so if we hit this it means our tracked queries and + // the server cache are out of sync. + NSAssert([pruneForest shouldKeepPath:relativePath], @"We have data at %@ that is neither pruned nor kept.", relativePath); + } + } + }]; +} + +- (NSArray *)loadTrackedQueries { + return self.trackedQueries.allValues; +} + +- (void)removeTrackedQuery:(NSUInteger)queryId { + [self.trackedQueries removeObjectForKey:@(queryId)]; + [self.trackedQueryKeys removeObjectForKey:@(queryId)]; +} + +- (void)saveTrackedQuery:(FTrackedQuery *)query { + self.trackedQueries[@(query.queryId)] = query; +} + +- (void)setTrackedQueryKeys:(NSSet *)keys forQueryId:(NSUInteger)queryId { + self.trackedQueryKeys[@(queryId)] = keys; +} + +- (void)updateTrackedQueryKeysWithAddedKeys:(NSSet *)added removedKeys:(NSSet *)removed forQueryId:(NSUInteger)queryId { + NSSet *oldKeys = [self trackedQueryKeysForQuery:queryId]; + NSMutableSet *newKeys = [NSMutableSet setWithSet:oldKeys]; + [newKeys minusSet:removed]; + [newKeys unionSet:added]; + self.trackedQueryKeys[@(queryId)] = newKeys; +} + +- (NSSet *)trackedQueryKeysForQuery:(NSUInteger)queryId { + NSSet *keys = self.trackedQueryKeys[@(queryId)]; + return keys != nil ? keys : [NSSet set]; +} + +@end diff --git a/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h new file mode 100644 index 0000000..d6d9fd3 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.h @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +@interface FTestAuthTokenGenerator : NSObject + ++ (NSString *) tokenWithSecret:(NSString *)secret authData:(NSDictionary *)data andOptions:(NSDictionary *)options; + +@end diff --git a/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m new file mode 100644 index 0000000..bd98e82 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestAuthTokenGenerator.m @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <CommonCrypto/CommonHMAC.h> +#import "FTestAuthTokenGenerator.h" +#import "Base64.h" + +@implementation FTestAuthTokenGenerator + ++ (NSString *) jsonStringForData:(id)data { + NSData* jsonData = [NSJSONSerialization dataWithJSONObject:data + options:kNilOptions error:nil]; + + return [[NSString alloc] initWithData:jsonData + encoding:NSUTF8StringEncoding]; +} + ++ (NSNumber *) tokenVersion { + return @0; +} + ++ (NSMutableDictionary *) createOptionsClaims:(NSDictionary *)options { + NSMutableDictionary* claims = [[NSMutableDictionary alloc] init]; + if (options) { + NSDictionary* map = @{ + @"expires": @"exp", + @"notBefore": @"nbf", + @"admin": @"admin", + @"debug": @"debug", + @"simulate": @"simulate" + }; + + for (NSString* claim in map) { + if (options[claim] != nil) { + NSString* claimName = [map objectForKey:claim]; + id val = [options objectForKey:claim]; + [claims setObject:val forKey:claimName]; + } + } + } + return claims; +} + ++ (NSString *) webSafeBase64:(NSString *)encoded { + return [[[encoded stringByReplacingOccurrencesOfString:@"=" withString:@""] stringByReplacingOccurrencesOfString:@"+" withString:@"-"] stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; +} + ++ (NSString *) base64EncodeString:(NSString *)target { + return [self webSafeBase64:[target base64EncodedString]]; +} + ++ (NSString *) tokenWithClaims:(NSDictionary *)claims andSecret:(NSString *)secret { + NSDictionary* headerData = @{@"typ": @"JWT", @"alg": @"HS256"}; + NSString* encodedHeader = [self base64EncodeString:[self jsonStringForData:headerData]]; + NSString* encodedClaims = [self base64EncodeString:[self jsonStringForData:claims]]; + + NSString* secureBits = [NSString stringWithFormat:@"%@.%@", encodedHeader, encodedClaims]; + + const char *cKey = [secret cStringUsingEncoding:NSUTF8StringEncoding]; + const char *cData = [secureBits cStringUsingEncoding:NSUTF8StringEncoding]; + unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH]; + CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), cData, strlen(cData), cHMAC); + NSData* hmac = [NSData dataWithBytesNoCopy:cHMAC length:CC_SHA256_DIGEST_LENGTH freeWhenDone:NO]; + NSString* encodedHmac = [self webSafeBase64:[hmac base64EncodedString]]; + return [NSString stringWithFormat:@"%@.%@.%@", encodedHeader, encodedClaims, encodedHmac]; +} + ++ (NSString *) tokenWithSecret:(NSString *)secret authData:(NSDictionary *)data andOptions:(NSDictionary *)options { + NSMutableDictionary* claims = [self createOptionsClaims:options]; + [claims setObject:[self tokenVersion] forKey:@"v"]; + NSNumber* now = [NSNumber numberWithDouble:[[NSDate date] timeIntervalSince1970]]; + [claims setObject:now forKey:@"iat"]; + [claims setObject:data forKey:@"d"]; + return [self tokenWithClaims:claims andSecret:secret]; +} + +@end diff --git a/Example/Database/Tests/Helpers/FTestBase.h b/Example/Database/Tests/Helpers/FTestBase.h new file mode 100644 index 0000000..8137b94 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestBase.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FTestHelpers.h" +#import "SenTest+FWaiter.h" + +@interface FTestBase : XCTestCase { + BOOL runPerfTests; +} + +- (void)snapWaiter:(FIRDatabaseReference *)path withBlock:(fbt_void_datasnapshot)fn; +- (void)waitUntilConnected:(FIRDatabaseReference *)ref; +- (void)waitForQueue:(FIRDatabaseReference *)ref; +- (void)waitForEvents:(FIRDatabaseReference *)ref; +- (void)waitForRoundTrip:(FIRDatabaseReference *)ref; +- (void)waitForValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected; +- (void)waitForExportValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected; +- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value; +- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value andPriority:(id)priority; +- (void)waitForCompletionOf:(FIRDatabaseReference *)ref updateChildValues:(NSDictionary *)values; + +@property(nonatomic, readonly) NSString *databaseURL; + +@end diff --git a/Example/Database/Tests/Helpers/FTestBase.m b/Example/Database/Tests/Helpers/FTestBase.m new file mode 100644 index 0000000..f55c73b --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestBase.m @@ -0,0 +1,170 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRApp.h" +#import "FIROptions.h" +#import "FTestBase.h" +#import "FTestAuthTokenGenerator.h" +#import "FIRDatabaseQuery_Private.h" +#import "FIRTestAuthTokenProvider.h" + +@implementation FTestBase + ++ (void)setUp +{ + static dispatch_once_t once; + dispatch_once(&once, ^ { + [FIRApp configure]; + }); +} + +- (void)setUp +{ + [super setUp]; + + [FIRDatabase setLoggingEnabled:YES]; + _databaseURL = [[FIRApp defaultApp] options].databaseURL; + + // Disabled normally since they slow down the tests and don't actually assert anything (they just NSLog timings). + runPerfTests = NO; +} + +- (void)snapWaiter:(FIRDatabaseReference *)path withBlock:(fbt_void_datasnapshot)fn { + __block BOOL done = NO; + + [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snap) { + fn(snap); + done = YES; + }]; + + NSTimeInterval timeTaken = [self waitUntil:^BOOL{ + return done; + } timeout:kFirebaseTestWaitUntilTimeout]; + + NSLog(@"snapWaiter:withBlock: timeTaken:%f", timeTaken); + + XCTAssertTrue(done, @"Properly finished."); +} + +- (void) waitUntilConnected:(FIRDatabaseReference *)ref { + __block BOOL connected = NO; + FIRDatabaseHandle handle = [[ref.root child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + connected = [snapshot.value boolValue]; + }]; + WAIT_FOR(connected); + [ref.root removeObserverWithHandle:handle]; +} + +- (void) waitForRoundTrip:(FIRDatabaseReference *)ref { + // HACK: Do a deep setPriority (which we expect to fail because there's no data there) to do a no-op roundtrip. + __block BOOL done = NO; + [[ref.root child:@"ENTOHTNUHOE/ONTEHNUHTOE"] setPriority:@"blah" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + WAIT_FOR(done); +} + +- (void) waitForQueue:(FIRDatabaseReference *)ref { + dispatch_sync([FIRDatabaseQuery sharedQueue], ^{}); +} + +- (void) waitForEvents:(FIRDatabaseReference *)ref { + [self waitForQueue:ref]; + __block BOOL done = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + done = YES; + }); + WAIT_FOR(done); +} + +- (void)waitForValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected { + __block id value; + FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + value = snapshot.value; + }]; + + @try { + [self waitUntil:^BOOL { + return [value isEqual:expected]; + }]; + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"DidNotGetValue" reason:@"Did not get expected value" + userInfo:@{ @"expected": (!expected ? @"nil" : expected), + @"actual": (!value ? @"nil" : value) }]; + } @finally { + [ref removeObserverWithHandle:handle]; + } +} + +- (void)waitForExportValueOf:(FIRDatabaseQuery *)ref toBe:(id)expected { + __block id value; + FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + value = snapshot.valueInExportFormat; + }]; + + @try { + [self waitUntil:^BOOL { + return [value isEqual:expected]; + }]; + } @catch (NSException *exception) { + if ([exception.name isEqualToString:@"Timed out"]) { + @throw [NSException exceptionWithName:@"DidNotGetValue" reason:@"Did not get expected value" + userInfo:@{ @"expected": (!expected ? @"nil" : expected), + @"actual": (!value ? @"nil" : value) }]; } else { + @throw exception; + } + } @finally { + [ref removeObserverWithHandle:handle]; + } +} + +- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value { + [self waitForCompletionOf:ref setValue:value andPriority:nil]; +} + +- (void)waitForCompletionOf:(FIRDatabaseReference *)ref setValue:(id)value andPriority:(id)priority { + __block BOOL done = NO; + [ref setValue:value andPriority:priority withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + + @try { + WAIT_FOR(done); + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"DidNotSetValue" reason:@"Did not complete setting value" + userInfo:@{ @"ref": [ref description], + @"done": done ? @"true" : @"false", + @"value": (!value ? @"nil" : value), + @"priority": (!priority ? @"nil" : priority) }]; + } +} + +- (void)waitForCompletionOf:(FIRDatabaseReference *)ref updateChildValues:(NSDictionary *)values { + __block BOOL done = NO; + [ref updateChildValues:values withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + + @try { + WAIT_FOR(done); + } @catch (NSException *exception) { + @throw [NSException exceptionWithName:@"DidNotUpdateChildValues" reason:@"Could not finish updating child values" + userInfo:@{ @"ref": [ref description], + @"values": (!values ? @"nil" : values)}]; + } +} + +@end diff --git a/Example/Database/Tests/Helpers/FTestCachePolicy.h b/Example/Database/Tests/Helpers/FTestCachePolicy.h new file mode 100644 index 0000000..688c21d --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestCachePolicy.h @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "FCachePolicy.h" + +@interface FTestCachePolicy : NSObject<FCachePolicy> + +- (id)initWithPercent:(float)percent maxQueries:(NSUInteger)maxQueries; + +- (void)pruneOnNextCheck; + +@end diff --git a/Example/Database/Tests/Helpers/FTestCachePolicy.m b/Example/Database/Tests/Helpers/FTestCachePolicy.m new file mode 100644 index 0000000..aacd010 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestCachePolicy.m @@ -0,0 +1,65 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestCachePolicy.h" + +@interface FTestCachePolicy () + + +@property (nonatomic) float percentOfQueries; +@property (nonatomic) NSUInteger maxTrackedQueries; +@property (nonatomic) BOOL pruneNext; + +@end + +@implementation FTestCachePolicy + +- (id)initWithPercent:(float)percent maxQueries:(NSUInteger)maxQueries { + self = [super init]; + if (self != nil) { + self->_maxTrackedQueries = maxQueries; + self->_percentOfQueries = percent; + self->_pruneNext = NO; + } + return self; +} + +- (void)pruneOnNextCheck { + self.pruneNext = YES; +} + +- (BOOL)shouldPruneCacheWithSize:(NSUInteger)cacheSize numberOfTrackedQueries:(NSUInteger)numTrackedQueries { + if (self.pruneNext) { + self.pruneNext = NO; + return YES; + } else { + return NO; + } +} + +- (BOOL)shouldCheckCacheSize:(NSUInteger)serverUpdatesSinceLastCheck { + return YES; +} + +- (float)percentOfQueriesToPruneAtOnce { + return self.percentOfQueries; +} + +- (NSUInteger)maxNumberOfQueriesToKeep { + return self.maxTrackedQueries; +} + +@end diff --git a/Example/Database/Tests/Helpers/FTestClock.h b/Example/Database/Tests/Helpers/FTestClock.h new file mode 100644 index 0000000..5520c6a --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestClock.h @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "FClock.h" + +@interface FTestClock : NSObject<FClock> + +@property (nonatomic, readonly) NSTimeInterval currentTime; + +- (id)init; +- (void)tick; + +@end diff --git a/Example/Database/Tests/Helpers/FTestClock.m b/Example/Database/Tests/Helpers/FTestClock.m new file mode 100644 index 0000000..43599ac --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestClock.m @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestClock.h" + +@implementation FTestClock + +- (id)init { + self = [super init]; + if (self != nil) { + self->_currentTime = 0.001; + } + return self; +} + +- (void)tick { + self->_currentTime = self->_currentTime + 0.001; +} + +@end diff --git a/Example/Database/Tests/Helpers/FTestContants.h b/Example/Database/Tests/Helpers/FTestContants.h new file mode 100644 index 0000000..bc8dd8d --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestContants.h @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef Firebase_FTestContants_h +#define Firebase_FTestContants_h + +#define kFirebaseTestTimeout 7 +#define kFirebaseTestWaitUntilTimeout 5 + +#endif diff --git a/Example/Database/Tests/Helpers/FTestExpectations.h b/Example/Database/Tests/Helpers/FTestExpectations.h new file mode 100644 index 0000000..8a797c8 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestExpectations.h @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> +#import "FIRDatabaseQuery.h" + +@interface FTestExpectations : XCTestCase { + NSMutableArray* expectations; + XCTestCase* from; +} + +- (id) initFrom:(XCTestCase *)other; +- (void)addQuery:(FIRDatabaseQuery *)query withExpectation:(id)expectation; +- (void) validate; + +@property (readonly) BOOL isReady; + +@end diff --git a/Example/Database/Tests/Helpers/FTestExpectations.m b/Example/Database/Tests/Helpers/FTestExpectations.m new file mode 100644 index 0000000..d0f84d7 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestExpectations.m @@ -0,0 +1,88 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestExpectations.h" +#import "FIRDataSnapshot.h" + +@interface FExpectation : NSObject + +@property (strong, nonatomic) FIRDatabaseQuery * query; +@property (strong, nonatomic) id expectation; +@property (strong, nonatomic) FIRDataSnapshot * snap; + +@end + +@implementation FExpectation + +@synthesize query; +@synthesize expectation; +@synthesize snap; + +@end + +@implementation FTestExpectations + +- (id) initFrom:(XCTestCase *)other { + self = [super init]; + if (self) { + expectations = [[NSMutableArray alloc] init]; + from = other; + } + return self; +} + +- (void)addQuery:(FIRDatabaseQuery *)query withExpectation:(id)expectation { + FExpectation* exp = [[FExpectation alloc] init]; + exp.query = query; + exp.expectation = expectation; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + exp.snap = snapshot; + }]; + [expectations addObject:exp]; +} + +- (BOOL) isReady { + for (FExpectation* exp in expectations) { + if (!exp.snap) { + return NO; + } + // Note that a failure here will end up triggering the timeout + FIRDataSnapshot * snap = exp.snap; + NSDictionary* result = snap.value; + NSDictionary* expected = exp.expectation; + if ([result isEqual:[NSNull null]] || ![result isEqualToDictionary:expected]) { + return NO; + } + } + return YES; +} + +- (void) validate { + for (FExpectation* exp in expectations) { + FIRDataSnapshot * snap = exp.snap; + NSDictionary* result = [snap value]; + NSDictionary* expected = exp.expectation; + XCTAssertTrue([result isEqualToDictionary:expected], @"Expectation mismatch: %@ should be %@", result, expected); + } +} + +- (void) failWithException:(NSException *) anException { + @throw anException; + // TODO: fix + //[from failWithException:anException]; +} + +@end diff --git a/Example/Database/Tests/Helpers/FTestHelpers.h b/Example/Database/Tests/Helpers/FTestHelpers.h new file mode 100644 index 0000000..679be7e --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestHelpers.h @@ -0,0 +1,38 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> +#import "FTupleFirebase.h" +#import "FRepoManager.h" +#import "FIRDatabaseReference_Private.h" +#import "FTestContants.h" +#import "FSnapshotUtilities.h" + +#define WAIT_FOR(x) [self waitUntil:^{ return (BOOL)(x); }]; + +#define NODE(__node) [FSnapshotUtilities nodeFrom:(__node)] +#define PATH(__path) [FPath pathWithString:(__path)] + +@interface FTestHelpers : XCTestCase ++ (FIRDatabaseReference *) getRandomNode; ++ (FIRDatabaseReference *) getRandomNodeWithoutPersistence; ++ (FTupleFirebase *) getRandomNodePair; ++ (FTupleFirebase *) getRandomNodePairWithoutPersistence; ++ (FTupleFirebase *) getRandomNodeTriple; ++ (id<FNode>)leafNodeOfSize:(NSUInteger)size; + +@end diff --git a/Example/Database/Tests/Helpers/FTestHelpers.m b/Example/Database/Tests/Helpers/FTestHelpers.m new file mode 100644 index 0000000..8ffdc7d --- /dev/null +++ b/Example/Database/Tests/Helpers/FTestHelpers.m @@ -0,0 +1,132 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestHelpers.h" +#import "FConstants.h" +#import "FIRApp.h" +#import "FIROptions.h" +#import "FIRDatabaseConfig_Private.h" +#import "FTestAuthTokenGenerator.h" + +@implementation FTestHelpers + ++ (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds { + NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; + NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:seconds]; + NSTimeInterval timeoutTime = [timeoutDate timeIntervalSinceReferenceDate]; + NSTimeInterval currentTime; + + for (currentTime = [NSDate timeIntervalSinceReferenceDate]; + !predicate() && currentTime < timeoutTime; + currentTime = [NSDate timeIntervalSinceReferenceDate]) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]]; + } + + NSTimeInterval finish = [NSDate timeIntervalSinceReferenceDate]; + + NSAssert(currentTime <= timeoutTime, @"Timed out"); + + return (finish - start); +} + ++ (NSArray*) getRandomNodes:(int)num persistence:(BOOL)persistence { + static dispatch_once_t pred = 0; + static NSMutableArray *persistenceRefs = nil; + static NSMutableArray *noPersistenceRefs = nil; + dispatch_once(&pred, ^{ + persistenceRefs = [[NSMutableArray alloc] init]; + noPersistenceRefs = [[NSMutableArray alloc] init]; + // Uncomment the following line to run tests against a background thread + //[Firebase setDispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; + }); + + NSMutableArray *refs = (persistence) ? persistenceRefs : noPersistenceRefs; + + id<FAuthTokenProvider> authTokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]]; + + while (num > refs.count) { + NSString *sessionIdentifier = [NSString stringWithFormat:@"test-config-%@persistence-%lu", (persistence) ? @"" : @"no-", refs.count]; + FIRDatabaseConfig *config = [[FIRDatabaseConfig alloc] initWithSessionIdentifier:sessionIdentifier authTokenProvider:authTokenProvider]; + config.persistenceEnabled = persistence; + FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:config]; + [refs addObject:ref]; + } + + NSMutableArray* results = [[NSMutableArray alloc] init]; + NSString* name = nil; + for (int i = 0; i < num; ++i) { + FIRDatabaseReference * ref = [refs objectAtIndex:i]; + if (!name) { + name = [ref childByAutoId].key; + } + [results addObject:[ref child:name]]; + } + return results; +} + +// Helpers ++ (FIRDatabaseReference *) getRandomNode { + NSArray* refs = [self getRandomNodes:1 persistence:YES]; + return [refs objectAtIndex:0]; +} + ++ (FIRDatabaseReference *) getRandomNodeWithoutPersistence { + NSArray* refs = [self getRandomNodes:1 persistence:NO]; + return refs[0]; +} + ++ (FTupleFirebase *) getRandomNodePair { + NSArray* refs = [self getRandomNodes:2 persistence:YES]; + + FTupleFirebase* tuple = [[FTupleFirebase alloc] init]; + tuple.one = [refs objectAtIndex:0]; + tuple.two = [refs objectAtIndex:1]; + + return tuple; +} + ++ (FTupleFirebase *) getRandomNodePairWithoutPersistence { + NSArray* refs = [self getRandomNodes:2 persistence:NO]; + + FTupleFirebase* tuple = [[FTupleFirebase alloc] init]; + tuple.one = refs[0]; + tuple.two = refs[1]; + + return tuple; +} + ++ (FTupleFirebase *) getRandomNodeTriple { + NSArray* refs = [self getRandomNodes:3 persistence:YES]; + FTupleFirebase* triple = [[FTupleFirebase alloc] init]; + triple.one = [refs objectAtIndex:0]; + triple.two = [refs objectAtIndex:1]; + triple.three = [refs objectAtIndex:2]; + + return triple; +} + ++ (id<FNode>)leafNodeOfSize:(NSUInteger)size { + NSMutableString *string = [NSMutableString string]; + NSString *pattern = @"abdefghijklmopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + for (NSUInteger i = 0; i < size - pattern.length; i = i + pattern.length) { + [string appendString:pattern]; + } + NSUInteger remainingLength = size - string.length; + [string appendString:[pattern substringToIndex:remainingLength]]; + return [FSnapshotUtilities nodeFrom:string]; +} + +@end diff --git a/Example/Database/Tests/Helpers/FTupleEventTypeString.h b/Example/Database/Tests/Helpers/FTupleEventTypeString.h new file mode 100644 index 0000000..adcb4a0 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTupleEventTypeString.h @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import "FIRDataEventType.h" +#import "FIRDatabaseReference.h" +#import "FTypedefs.h" + +@interface FTupleEventTypeString : NSObject + +- (id)initWithFirebase:(FIRDatabaseReference *)f withEvent:(FIRDataEventType)evt withString:(NSString *)str; +- (BOOL) isEqualTo:(FTupleEventTypeString *)other; + +@property (nonatomic, strong) FIRDatabaseReference * firebase; +@property (readwrite) FIRDataEventType eventType; +@property (nonatomic, strong) NSString* string; +@property (nonatomic, copy) fbt_void_void vvcallback; +@property (nonatomic) BOOL initialized; + +@end diff --git a/Example/Database/Tests/Helpers/FTupleEventTypeString.m b/Example/Database/Tests/Helpers/FTupleEventTypeString.m new file mode 100644 index 0000000..4cb3df2 --- /dev/null +++ b/Example/Database/Tests/Helpers/FTupleEventTypeString.m @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTupleEventTypeString.h" + +@implementation FTupleEventTypeString + +@synthesize firebase; +@synthesize eventType; +@synthesize string; +@synthesize vvcallback; +@synthesize initialized; + +- (id)initWithFirebase:(FIRDatabaseReference *)f withEvent:(FIRDataEventType)evt withString:(NSString *)str; +{ + self = [super init]; + if (self) { + self.firebase = f; + self.eventType = evt; + self.string = str; + self.initialized = NO; + } + return self; +} + +- (NSString *) description { + return [NSString stringWithFormat:@"%@ %@ (%zd)", self.firebase, self.string, self.eventType]; +} + +- (BOOL) isEqualTo:(FTupleEventTypeString *)other { + BOOL stringsEqual = NO; + if (self.string == nil && other.string == nil) { + stringsEqual = YES; + } else if (self.string != nil && other.string != nil) { + stringsEqual = [self.string isEqualToString:other.string]; + } + return self.eventType == other.eventType && stringsEqual && [[self.firebase description] isEqualToString:[other.firebase description]]; +} + +@end diff --git a/Example/Database/Tests/Helpers/SenTest+FWaiter.h b/Example/Database/Tests/Helpers/SenTest+FWaiter.h new file mode 100644 index 0000000..81556df --- /dev/null +++ b/Example/Database/Tests/Helpers/SenTest+FWaiter.h @@ -0,0 +1,26 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +@interface XCTest (FWaiter) + +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate; +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate description:(NSString*)desc; +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds; +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds description:(NSString*)desc; + +@end diff --git a/Example/Database/Tests/Helpers/SenTest+FWaiter.m b/Example/Database/Tests/Helpers/SenTest+FWaiter.m new file mode 100644 index 0000000..4c5c854 --- /dev/null +++ b/Example/Database/Tests/Helpers/SenTest+FWaiter.m @@ -0,0 +1,57 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "SenTest+FWaiter.h" +#import "FTestContants.h" + +@implementation XCTestCase (FWaiter) + +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate { + return [self waitUntil:predicate timeout:kFirebaseTestWaitUntilTimeout description:nil]; +} + +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate description:(NSString*)desc { + return [self waitUntil:predicate timeout:kFirebaseTestWaitUntilTimeout description:desc]; +} + +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds { + return [self waitUntil:predicate timeout:seconds description:nil]; +} + +- (NSTimeInterval) waitUntil:(BOOL (^)())predicate timeout:(NSTimeInterval)seconds description:(NSString*)desc { + NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; + NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:seconds]; + NSTimeInterval timeoutTime = [timeoutDate timeIntervalSinceReferenceDate]; + NSTimeInterval currentTime; + + for (currentTime = [NSDate timeIntervalSinceReferenceDate]; + !predicate() && currentTime < timeoutTime; + currentTime = [NSDate timeIntervalSinceReferenceDate]) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.25]]; + } + + NSTimeInterval finish = [NSDate timeIntervalSinceReferenceDate]; + if (currentTime > timeoutTime) { + if (desc != nil) { + XCTFail("Timed out on: %@", desc); + } else { + XCTFail("Timed out"); + } + } + return (finish - start); +} + +@end diff --git a/Example/Database/Tests/Integration/FConnectionTest.m b/Example/Database/Tests/Integration/FConnectionTest.m new file mode 100644 index 0000000..e72f6e4 --- /dev/null +++ b/Example/Database/Tests/Integration/FConnectionTest.m @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "FIRApp.h" +#import "FIROptions.h" +#import "FTestHelpers.h" +#import "FConnection.h" +#import "FTestBase.h" +#import "FIRDatabaseQuery_Private.h" + +@interface FConnectionTest : FTestBase + +@end + +@interface FTestConnectionDelegate : NSObject<FConnectionDelegate> + +@property (nonatomic, copy) void (^onReady)(NSString *); +@property (nonatomic, copy) void (^onDisconnect)(FDisconnectReason); + +@end + +@implementation FTestConnectionDelegate + +- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID{ + self.onReady(sessionID); +} +- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message {} +- (void)onDisconnect:(FConnection *)fwebSocket withReason:(FDisconnectReason)reason { + self.onDisconnect(reason); +} +- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason {} + +@end +@implementation FConnectionTest + +-(void) XXXtestObtainSessionId { + NSString* host = [NSString stringWithFormat:@"%@.firebaseio.com", [[FIRApp defaultApp] options].projectID]; + FRepoInfo *info = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:@"default"]; + FConnection *conn = [[FConnection alloc] initWith:info andDispatchQueue:[FIRDatabaseQuery sharedQueue] lastSessionID:nil]; + FTestConnectionDelegate *delegate = [[FTestConnectionDelegate alloc] init]; + + __block BOOL done = NO; + + delegate.onDisconnect = ^(FDisconnectReason reason) { + if (reason == DISCONNECT_REASON_SERVER_RESET) { + // It is very likely that the first connection attempt sends us a redirect to the project's designated server. + // We need follow that redirect before 'onReady' is invoked. + [conn open]; + } + }; + delegate.onReady = ^(NSString *sessionID) { + NSAssert(sessionID, @"sessionID cannot be null"); + NSAssert([sessionID length] != 0, @"sessionID must have length > 0"); + done = YES; + }; + + conn.delegate = delegate; + [conn open]; + + WAIT_FOR(done); +} +@end diff --git a/Example/Database/Tests/Integration/FData.h b/Example/Database/Tests/Integration/FData.h new file mode 100644 index 0000000..ebb502e --- /dev/null +++ b/Example/Database/Tests/Integration/FData.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FTestBase.h" + +@interface FData : FTestBase + +@end diff --git a/Example/Database/Tests/Integration/FData.m b/Example/Database/Tests/Integration/FData.m new file mode 100644 index 0000000..390522c --- /dev/null +++ b/Example/Database/Tests/Integration/FData.m @@ -0,0 +1,2687 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FData.h" +#import "FTestHelpers.h" +#import "FEventTester.h" +#import "FTupleEventTypeString.h" +#import "FIRApp.h" +#import "FIRDatabaseQuery_Private.h" +#import "FIRDatabaseConfig_Private.h" +#import "FIROptions.h" +#import "FRepo_Private.h" +#import <limits.h> + +@implementation FData + +- (void) testGetNode { + __unused FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + XCTAssertTrue(YES, @"Properly created node without throwing error"); +} + +- (void) testWriteData { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + [node setValue:@42]; + XCTAssertTrue(YES, @"Properly write to node without throwing error"); +} + +- (void) testWriteDataWithDebugLogging { + [FIRDatabase setLoggingEnabled:YES]; + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + [node setValue:@42]; + [FIRDatabase setLoggingEnabled:NO]; + XCTAssertTrue(YES, @"Properly write to node without throwing error"); +} + +- (void) testWriteAndReadData { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + [node setValue:@42]; + + [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw correct value"); + }]; +} + +- (void) testProperParamChecking { + // ios doesn't have an equivalent of this test +} + +- (void) testNamespaceCaseInsensitivityWithinARepo { + FIRDatabaseReference * ref1 = [[FIRDatabase database] referenceFromURL:[self.databaseURL uppercaseString]]; + FIRDatabaseReference * ref2 = [[FIRDatabase database] referenceFromURL:[self.databaseURL lowercaseString]]; + + XCTAssertTrue([ref1.description isEqualToString:ref2.description], @"Descriptions should match"); +} + +- (void) testRootProperty { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + FIRDatabaseReference * root = node.root; + XCTAssertTrue(root != nil, @"Should get a root"); + XCTAssertTrue([[root description] isEqualToString:self.databaseURL], @"Root is actually the root"); +} + +- (void) testValReturnsCompoundObjectWithChildren { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [node setValue:@{@"foo": @{@"bar": @5}}]; + + [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqualObjects([[[snapshot value] objectForKey:@"foo"] objectForKey:@"bar"], @5, @"Properly saw compound object"); + }]; +} + +- (void) testWriteDataAndWaitForServerConfirmation { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [self waitForCompletionOf:node setValue:@42]; +} + +- (void) testWriteAValueAndRead { + // dupe of FEvent testWriteLeafExpectValueChanged +} + +- (void) testWriteABunchOfDataAndRead { + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writeNode = tuple.one; + FIRDatabaseReference * readNode = tuple.two; + + + __block BOOL done = NO; + + [[[[writeNode child:@"a"] child:@"b"] child:@"c"] setValue:@1]; + [[[[writeNode child:@"a"] child:@"d"] child:@"e"] setValue:@2]; + [[[[writeNode child:@"a"] child:@"d"] child:@"f"] setValue:@3]; + [[writeNode child:@"g"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }]; + + [self waitUntil:^BOOL{ return done; }]; + + [super snapWaiter:readNode withBlock:^(FIRDataSnapshot *s) { + XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"b"] childSnapshotForPath:@"c"] value], @1, @"Proper child value"); + XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"d"] childSnapshotForPath:@"e"] value], @2, @"Proper child value"); + XCTAssertEqualObjects([[[[s childSnapshotForPath:@"a"] childSnapshotForPath:@"d"] childSnapshotForPath:@"f"] value], @3, @"Proper child value"); + XCTAssertEqualObjects([[s childSnapshotForPath:@"g"] value], @4, @"Proper child value"); + }]; +} + +- (void) testWriteABunchOfDataWithLeadingZeroesAndRead { + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writeNode = tuple.one; + FIRDatabaseReference * readNode = tuple.two; + + [self waitForCompletionOf:[writeNode child:@"1"] setValue:@1]; + [self waitForCompletionOf:[writeNode child:@"01"] setValue:@2]; + [self waitForCompletionOf:[writeNode child:@"001"] setValue:@3]; + [self waitForCompletionOf:[writeNode child:@"0001"] setValue:@4]; + + [super snapWaiter:readNode withBlock:^(FIRDataSnapshot *s) { + XCTAssertEqualObjects([[s childSnapshotForPath:@"1"] value], @1, @"Proper child value"); + XCTAssertEqualObjects([[s childSnapshotForPath:@"01"] value], @2, @"Proper child value"); + XCTAssertEqualObjects([[s childSnapshotForPath:@"001"] value], @3, @"Proper child value"); + XCTAssertEqualObjects([[s childSnapshotForPath:@"0001"] value], @4, @"Proper child value"); + }]; +} + +- (void) testLeadingZeroesTurnIntoDictionary { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + [self waitForCompletionOf:[ref child:@"1"] setValue:@1]; + [self waitForCompletionOf:[ref child:@"01"] setValue:@2]; + + __block BOOL done = NO; + __block FIRDataSnapshot * snap = nil; + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + done = YES; + }]; + + WAIT_FOR(done); + + XCTAssertTrue([snap.value isKindOfClass:[NSDictionary class]], @"Should be dictionary"); + XCTAssertEqualObjects([snap.value objectForKey:@"1"], @1, @"Proper child value"); + XCTAssertEqualObjects([snap.value objectForKey:@"01"], @2, @"Proper child value"); +} + +- (void) testLeadingZerosDontCollapseLocally { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + __block FIRDataSnapshot * snap = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + done = (snapshot.childrenCount == 2); + }]; + + [[ref child:@"3"] setValue:@YES]; + [[ref child:@"03"] setValue:@NO]; + + WAIT_FOR(done); + + XCTAssertEqualObjects([[snap childSnapshotForPath:@"3"] value], @YES, @"Proper child value"); + XCTAssertEqualObjects([[snap childSnapshotForPath:@"03"] value], @NO, @"Proper child value"); +} + +- (void) testSnapshotRef { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [snapshot.ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = YES; + }]; + }]; + WAIT_FOR(done); +} + +- (void) testWriteLeafNodeOverwriteAtParentVerifyExpectedEvents { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + FIRDatabaseReference * connected = [[[FIRDatabase database] reference] child:@".info/connected"]; + __block BOOL ready = NO; + [connected observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSNumber *val = [snapshot value]; + ready = [val boolValue]; + }]; + + WAIT_FOR(ready); + + NSArray* lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], // 4 + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], // 0 + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], // 4 + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"], // 2 + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], // 4 + ]; + + [[node repo] interrupt]; // Going offline ensures that local events get queued up before server events + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:lookingFor]; + + [[node child:@"a/aa"] setValue:@1]; + [[node child:@"a"] setValue:@{@"aa": @2}]; + + [[node repo] resume]; + [et wait]; +} + +- (void) testWriteLeafNodeOverwriteAtParentMultipleTimesVerifyExpectedEvents { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSArray* lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/bb"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + ]; + + [[node repo] interrupt]; // Going offline ensures that local events get queued up before server events + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:lookingFor]; + + [[node child:@"a/aa"] setValue:@1]; + [[node child:@"a"] setValue:@{@"aa": @2}]; + [[node child:@"a"] setValue:@{@"aa": @3}]; + [[node child:@"a"] setValue:@{@"aa": @3}]; + + [[node repo] resume]; + [et wait]; +} + +- (void) testWriteParentNodeOverwriteAtLeafVerifyExpectedEvents { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSArray* lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeChildChanged withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + ]; + + [[node repo] interrupt]; // Going offline ensures that local events get queued up before server events + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:lookingFor]; + + [[node child:@"a"] setValue:@{@"aa": @2}]; + [[node child:@"a/aa"] setValue:@1]; + + [[node repo] resume]; + [et wait]; +} + +- (void) testWriteLeafNodeRemoveParentNodeVerifyExpectedEvents { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + NSArray* lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildAdded withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil], + ]; + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:lookingFor]; + + [[writer child:@"a/aa"] setValue:@42]; + // the local events + [et wait]; + + // the reader should get all of the events intermingled + FEventTester* readerEvents = [[FEventTester alloc] initFrom:self]; + lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildAdded withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [readerEvents addLookingFor:lookingFor]; + + [readerEvents wait]; + + lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildRemoved withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil] + ]; + [readerEvents addLookingFor:lookingFor]; + + lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildRemoved withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:lookingFor]; + + [[writer child:@"a"] removeValue]; + + [et wait]; + [readerEvents wait]; + + [et unregister]; + [readerEvents unregister]; + + // Ensure we can write a new value + __block NSNumber* readVal = @0.0; + __block NSNumber* writeVal = @0.0; + + [[reader child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + if (val != [NSNull null]) { + readVal = val; + } + }]; + + [[writer child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + if (val != [NSNull null]) { + writeVal = val; + } + }]; + + [[writer child:@"a/aa"] setValue:@3.1415]; + + [self waitUntil:^BOOL{ + return fabs([readVal doubleValue] - 3.1415) < 0.001 && fabs([writeVal doubleValue] - 3.1415) < 0.001; + //return [readVal isEqualToNumber:@3.1415] && [writeVal isEqualToNumber:@3.1415]; + }]; +} + +- (void) testWriteLeafNodeRemoveLeafVerifyExpectedEvents { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + NSArray* lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildAdded withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil], + ]; + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:lookingFor]; + [[writer child:@"a/aa"] setValue:@42]; + + // the local events + [et wait]; + + // the reader should get all of the events intermingled + FEventTester* readerEvents = [[FEventTester alloc] initFrom:self]; + lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildAdded withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildAdded withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [readerEvents addLookingFor:lookingFor]; + + [readerEvents wait]; + + lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildRemoved withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil] + ]; + [readerEvents addLookingFor:lookingFor]; + + lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeChildRemoved withString:@"aa"], + [[FTupleEventTypeString alloc] initWithFirebase:[writer child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeChildRemoved withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:writer withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:lookingFor]; + + // remove just the leaf + [[writer child:@"a/aa"] removeValue]; + + [et wait]; + [readerEvents wait]; + + [et unregister]; + [readerEvents unregister]; + + // Ensure we can write a new value + __block NSNumber* readVal = @0.0; + __block NSNumber* writeVal = @0.0; + + [[reader child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + if (val != [NSNull null]) { + readVal = val; + } + }]; + + [[writer child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + if (val != [NSNull null]) { + writeVal = val; + } + }]; + + [[writer child:@"a/aa"] setValue:@3.1415]; + + [self waitUntil:^BOOL{ + //NSLog(@"readVal: %@, writeVal: %@, vs %@", readVal, writeVal, @3.1415); + //return [readVal isEqualToNumber:@3.1415] && [writeVal isEqualToNumber:@3.1415]; + return fabs([readVal doubleValue] - 3.1415) < 0.001 && fabs([writeVal doubleValue] - 3.1415) < 0.001; + }]; +} + +- (void) testWriteMultipleLeafNodesRemoveOnlyOneVerifyExpectedEvents { + // XXX impl +} + +- (void) testVerifyNodeNamesCantStartWithADot { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + XCTAssertThrows([ref child:@".foo"], @"not a valid .prefix"); + XCTAssertThrows([ref child:@"foo/.foo"], @"not a valid path"); + // Should not throw + [[ref parent] child:@".info"]; +} + +- (void) testVerifyWritingToDotLengthAndDotKeysThrows { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + XCTAssertThrows([[ref child:@".keys"] setValue:@42], @"not a valid .prefix"); + XCTAssertThrows([[ref child:@".length"] setValue:@42], @"not a valid path"); +} + +- (void) testNumericKeysGetTurnedIntoArrays { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + [[ref child:@"0"] setValue:@"alpha"]; + [[ref child:@"1"] setValue:@"bravo"]; + [[ref child:@"2"] setValue:@"charlie"]; + [[ref child:@"3"] setValue:@"delta"]; + [[ref child:@"4"] setValue:@"echo"]; + + __block BOOL ready = NO; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + XCTAssertTrue([val isKindOfClass:[NSArray class]], @"Expected an array"); + NSArray *expected = @[@"alpha", @"bravo", @"charlie", @"delta", @"echo"]; + XCTAssertTrue([expected isEqualToArray:val], @"Did not get the correct array"); + ready = YES; + }]; + + [self waitUntil:^{ return ready; }]; +} + +// This was an issue on 64-bit. +- (void) testLargeNumericKeysDontGetTurnedIntoArrays { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + [[ref child:@"100003354884401"] setValue:@"alpha"]; + + __block BOOL ready = NO; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + XCTAssertTrue([val isKindOfClass:[NSDictionary class]], @"Expected a dictionary."); + ready = YES; + }]; + + [self waitUntil:^{ return ready; }]; +} + +- (void) testWriteCompoundObjectAndGetItBack { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSDictionary* data = @{ + @"a": @{@"aa": @5, + @"ab": @3}, + @"b": @{@"ba": @"hey there!", + @"bb": @{@"bba": @NO}}, + @"c": @[@0, + @{@"c_1": @4}, + @"hey", + @YES, + @NO, + @"dude"] + }; + + __block FIRDataSnapshot *snap = nil; + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + __block BOOL done = NO; + [node setValue:data withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([[[[snapshot value] objectForKey:@"c"] objectAtIndex:3] boolValue], @"Got proper boolean"); + }]; +} + +- (void) testCanPassValueToPush { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + FIRDatabaseReference * pushA = [node childByAutoId]; + [pushA setValue:@5]; + + [self snapWaiter:pushA withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqualObjects(@5, [snapshot value], @"Got proper value"); + }]; + + FIRDatabaseReference * pushB = [node childByAutoId]; + [pushB setValue:@{@"a": @5, @"b": @6}]; + + [self snapWaiter:pushB withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqualObjects(@5, [[snapshot value] objectForKey:@"a"], @"Got proper value"); + XCTAssertEqualObjects(@6, [[snapshot value] objectForKey:@"b"], @"Got proper value"); + }]; +} + +// Dropped test that tested callbacks to push. Support was removed. + +- (void) testRemoveCallbackHit { + + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + __block BOOL setDone = NO; + __block BOOL removeDone = NO; + __block BOOL readDone = NO; + + [node setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + setDone = YES; + }]; + + [self waitUntil:^BOOL{ + return setDone; + }]; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + if (val == [NSNull null]) { + readDone = YES; + } + }]; + + [node removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + XCTAssertTrue(error == nil, @"Should not be an error removing"); + removeDone = YES; + }]; + + [self waitUntil:^BOOL{ + return readDone && removeDone; + }]; +} + +- (void) testRemoveCallbackIsHitForNodesThatAreAlreadyRemoved { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block int removes = 0; + + [node removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + removes = removes + 1; + }]; + + [node removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + removes = removes + 1; + }]; + + [self waitUntil:^BOOL{ + return removes == 2; + }]; +} + +- (void) testUsingNumbersAsKeysDoesntCreateHugeSparseArrays { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + [[ref child:@"3024"] setValue:@5]; + + __block BOOL ready = NO; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + XCTAssertTrue(![val isKindOfClass:[NSArray class]], @"Should not be an array"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testOnceWithACallbackHitsServer { + FTupleFirebase* tuple = [FTestHelpers getRandomNodeTriple]; + FIRDatabaseReference * writeNode = tuple.one; + FIRDatabaseReference * readNode = tuple.two; + FIRDatabaseReference * readNodeB = tuple.three; + + __block BOOL initialReadDone = NO; + + [readNode observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"First callback is null"); + initialReadDone = YES; + }]; + + [self waitUntil:^BOOL{ + return initialReadDone; + }]; + + __block BOOL writeDone = NO; + + [writeNode setValue:@42 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + writeDone = YES; + }]; + + [self waitUntil:^BOOL{ + return writeDone; + }]; + + __block BOOL readDone = NO; + + [readNodeB observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqualObjects(@42, [snapshot value], @"Proper second read"); + readDone = YES; + }]; + + [self waitUntil:^BOOL{ + return readDone; + }]; +} + +// Removed test of forEach aborting iteration. Support dropped, use for .. in syntax + +- (void) testSetAndThenListenForValueEventsAreCorrect { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL setDone = NO; + + [node setValue:@"moo" withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + setDone = YES; + }]; + + __block int calls = 0; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + calls = calls + 1; + XCTAssertTrue(calls == 1, @"Only called once"); + XCTAssertEqualObjects([snapshot value], @"moo", @"Proper snapshot value"); + }]; + + [self waitUntil:^BOOL{ + return setDone && calls == 1; + }]; +} + +- (void) testHasChildrenWorksCorrectly { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [node setValue:@{@"one" : @42, @"two": @{@"a": @5}, @"three": @{@"a": @5, @"b": @6}}]; + + __block BOOL removedTwo = NO; + __block BOOL done = NO; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (!removedTwo) { + XCTAssertFalse([[snapshot childSnapshotForPath:@"one"] hasChildren], @"nope"); + XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] hasChildren], @"nope"); + XCTAssertTrue([[snapshot childSnapshotForPath:@"three"] hasChildren], @"nope"); + XCTAssertFalse([[snapshot childSnapshotForPath:@"four"] hasChildren], @"nope"); + + removedTwo = YES; + [[node child:@"two"] removeValue]; + } + else { + XCTAssertFalse([[snapshot childSnapshotForPath:@"two"] hasChildren], @"Second time around"); + done = YES; + } + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testNumChildrenWorksCorrectly { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [node setValue:@{@"one" : @42, @"two": @{@"a": @5}, @"three": @{@"a": @5, @"b": @6}}]; + + __block BOOL removedTwo = NO; + __block BOOL done = NO; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (!removedTwo) { + XCTAssertTrue([snapshot childrenCount] == 3, @"Total children"); + XCTAssertTrue([[snapshot childSnapshotForPath:@"one"] childrenCount] == 0, @"Two's children"); + XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] childrenCount] == 1, @"Two's children"); + XCTAssertTrue([[snapshot childSnapshotForPath:@"three"] childrenCount] == 2, @"Two's children"); + XCTAssertTrue([[snapshot childSnapshotForPath:@"four"] childrenCount] == 0, @"Two's children"); + + removedTwo = YES; + [[node child:@"two"] removeValue]; + } + else { + XCTAssertTrue([snapshot childrenCount] == 2, @"Total children"); + XCTAssertTrue([[snapshot childSnapshotForPath:@"two"] childrenCount] == 0, @"Two's children"); + done = YES; + } + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testSettingANodeWithChildrenToAPrimitiveAndBack { + // Can't tolerate stale data; so disable persistence. + FTupleFirebase* tuple = [FTestHelpers getRandomNodePairWithoutPersistence]; + FIRDatabaseReference * writeNode = tuple.one; + FIRDatabaseReference * readNode = tuple.two; + + __block BOOL done = NO; + + NSDictionary* compound = @{@"a": @5, @"b": @6}; + NSNumber* number = @76; + + [writeNode setValue:compound]; + + [self snapWaiter:writeNode withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([snapshot hasChildren], @"Has children"); + XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child"); + XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + [self snapWaiter:readNode withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([snapshot hasChildren], @"has children"); + XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child"); + XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + + [writeNode setValue:number withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + [self snapWaiter:readNode withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertFalse([snapshot hasChildren], @"No more children"); + XCTAssertEqualObjects(number, [snapshot value], @"Proper non compound value"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + [writeNode setValue:compound withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + [self snapWaiter:readNode withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([snapshot hasChildren], @"Has children"); + XCTAssertEqualObjects(@5, [[snapshot childSnapshotForPath:@"a"] value], @"First child"); + XCTAssertEqualObjects(@6, [[snapshot childSnapshotForPath:@"b"] value], @"First child"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + XCTAssertTrue(done, @"Properly finished"); +} + +- (void) testWriteLeafRemoveLeafAddChildToRemovedNode { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL ready = NO; + [writer setValue:@5]; + [writer removeValue]; + [[writer child:@"abc"] setValue:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + __block NSDictionary* readVal = nil; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + readVal = [snapshot value]; + }]; + + [self waitUntil:^BOOL{ + return readVal != nil; + }]; + + NSNumber* five = [readVal objectForKey:@"abc"]; + XCTAssertTrue([five isEqualToNumber:@5], @"Should get 5"); +} + +- (void) testListenForValueAndThenWriteOnANodeWithExistingData { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + [self waitForCompletionOf:writer setValue:@{@"a": @5, @"b": @2}]; + + __block int calls = 0; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + calls++; + if (calls == 1) { + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"a" : @10, @"b" : @2}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got the correct value"); + } else { + XCTFail(@"Should only be called once"); + } + }]; + + [[reader child:@"a"] setValue:@10]; + [self waitUntil:^BOOL{ + return calls == 1; + }]; +} + +- (void) testSetPriorityOnNonexistentNodeFails { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setPriority:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + XCTAssertTrue(error != nil, @"This should not succeed"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testSetPriorityOnExistentNodeSucceeds { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@"hello!"]; + [ref setPriority:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + XCTAssertTrue(error == nil, @"This should succeed"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testSetWithPrioritySetsValueAndPriority { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + [self waitForCompletionOf:writer setValue:@"hello" andPriority:@5]; + + __block FIRDataSnapshot * writeSnap = nil; + __block FIRDataSnapshot * readSnap = nil; + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + writeSnap = snapshot; + }]; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + readSnap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return readSnap != nil && writeSnap != nil; + }]; + + XCTAssertTrue([@"hello" isEqualToString:[readSnap value]], @"Got the value on the reader"); + XCTAssertTrue([@"hello" isEqualToString:[writeSnap value]], @"Got the value on the writer"); + XCTAssertTrue([@5 isEqualToNumber:[readSnap priority]], @"Got the priority on the reader"); + XCTAssertTrue([@5 isEqualToNumber:[writeSnap priority]], @"Got the priority on the writer"); +} + +- (void) testEffectsOfSetPriorityIsImmediatelyEvident { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSMutableArray* values = [[NSMutableArray alloc] init]; + NSMutableArray* priorities = [[NSMutableArray alloc] init]; + + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [values addObject:[snapshot value]]; + [priorities addObject:[snapshot priority]]; + }]; + [ref setValue:@5]; + [ref setPriority:@10]; + __block BOOL ready = NO; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [values addObject:[snapshot value]]; + [priorities addObject:[snapshot priority]]; + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSArray* expectedValues = @[@5, @5]; + NSArray* expectedPriorites = @[[NSNull null], @10]; + XCTAssertTrue([values isEqualToArray:expectedValues], @"Expected both listeners to get 5, got %@ instead", values); + XCTAssertTrue([priorities isEqualToArray:expectedPriorites], @"The first listener should have missed the priority, got %@ instead", priorities); +} + +- (void) testSetOverwritesPriorityOfTopLevelNodeAndSubnodes { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL ready = NO; + [writer setValue:@{@"a": @5}]; + [writer setPriority:@10]; + [[writer child:@"a"] setPriority:@18]; + [writer setValue:@{@"a": @7} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id pri = [snapshot priority]; + XCTAssertTrue([NSNull null] == pri, @"Expected null priority"); + FIRDataSnapshot *child = [snapshot childSnapshotForPath:@"a"]; + XCTAssertTrue([NSNull null] == [child priority], @"Child priority should be null too"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testSetPriorityOfLeafSavesCorrectly { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL ready = NO; + [writer setValue:@"testleaf" andPriority:@992 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id pri = [snapshot priority]; + XCTAssertTrue([@992 isEqualToNumber:pri], @"Expected non-null priority"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testSetPriorityOfObjectSavesCorrectly { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL ready = NO; + [writer setValue:@{@"a": @5} andPriority:@991 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id pri = [snapshot priority]; + XCTAssertTrue([@991 isEqualToNumber:pri], @"Expected non-null priority"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + + +- (void) testSetWithPriorityFollowedBySetClearsPriority { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL ready = NO; + [writer setValue:@{@"a": @5} andPriority:@991 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [reader setValue:@{@"a": @19} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id pri = [snapshot priority]; + XCTAssertTrue([NSNull null] == pri, @"Expected null priority"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testGetPriorityReturnsCorrectType { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + __block FIRDataSnapshot * snap = nil; + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [ref setValue:@"a"]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([snap priority] == [NSNull null], @"Expect null priority"); + snap = nil; + + [ref setValue:@"b" andPriority:@5]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap priority] isEqualToNumber:@5], @"Expect priority"); + snap = nil; + + [ref setValue:@"c" andPriority:@"6"]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap priority] isEqualToString:@"6"], @"Expect priority"); + snap = nil; + + [ref setValue:@"d" andPriority:@7]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap priority] isEqualToNumber:@7], @"Expect priority"); + snap = nil; + + [ref setValue:@{@".value": @"e", @".priority": @8}]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap priority] isEqualToNumber:@8], @"Expect priority"); + snap = nil; + + [ref setValue:@{@".value": @"f", @".priority": @"8"}]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap priority] isEqualToString:@"8"], @"Expect priority"); + snap = nil; + + [ref setValue:@{@".value": @"e", @".priority": [NSNull null]}]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([snap priority] == [NSNull null], @"Expect priority"); + snap = nil; + +} + +- (void) testExportValIncludesPriorities { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSDictionary* contents = @{@"foo": @{@"bar": @{@".value": @5, @".priority": @7}, @".priority": @"hi"}}; + __block FIRDataSnapshot * snap = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + [ref setValue:contents]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([contents isEqualToDictionary:[snap valueInExportFormat]], @"Expected priorities in snapshot"); +} + +- (void) testPriorityIsOverwrittenByServer { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block int event = 0; + __block BOOL done = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSLog(@"%@ Snapshot", snapshot); + id pri = [snapshot priority]; + if (event == 0) { + XCTAssertTrue([@100 isEqualToNumber:pri], @"Expect local priority. Got %@ instead.", pri); + } else if (event == 1) { + XCTAssertTrue(pri == [NSNull null], @"Expect remote priority. Got %@ instead.", pri); + } else { + XCTFail(@"Extra event"); + } + event++; + if (event == 2) { + done = YES; + } + }]; + + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id pri = [snapshot priority]; + if ([[pri class] isSubclassOfClass:[NSNumber class]] && [@100 isEqualToNumber:pri]) { + [writer setValue:@"whatever"]; + } + }]; + + [reader setValue:@"hi" andPriority:@100]; + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testLargeNumericPrioritiesWork { + NSNumber* bigPriority = @1356721306842; + __block BOOL ready = NO; + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + [self waitForCompletionOf:writer setValue:@5 andPriority:bigPriority]; + + __block NSNumber* serverPriority = @0; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + serverPriority = [snapshot priority]; + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([bigPriority isEqualToNumber:serverPriority], @"Expect big priority back"); +} + +- (void) testToString { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FIRDatabaseReference * parent = [ref parent]; + + XCTAssertTrue([[parent description] isEqualToString:self.databaseURL], @"Expect domain"); + FIRDatabaseReference * child = [parent child:@"a/b/c"]; + NSString* expected = [NSString stringWithFormat:@"%@/a/b/c", self.databaseURL]; + XCTAssertTrue([[child description] isEqualToString:expected], @"Expected path"); +} + +- (void) testURLEncodingOfDescriptionAndURLDecodingOfNewFirebase { + __block BOOL ready = NO; + NSString* test1 = [NSString stringWithFormat:@"%@/a%%b&c@d/space: /non-ascii_character:ø", self.databaseURL]; + NSString* expected1 = [NSString stringWithFormat:@"%@/a%%25b%%26c%%40d/space%%3A%%20/non-ascii_character%%3A%%C3%%B8", self.databaseURL]; + FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:test1]; + NSString* result = [ref description]; + XCTAssertTrue([result isEqualToString:expected1], @"Encodes properly"); + + int rnd = arc4random_uniform(100000000); + NSString* path = [NSString stringWithFormat:@"%i", rnd]; + [[ref child:path] setValue:@"testdata" withCompletionBlock:^(NSError* error, FIRDatabaseReference * childRef) { + FIRDatabaseReference * other = [[FIRDatabase database] referenceFromURL:[ref description]]; + [[other child:path] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSString *val = snapshot.value; + XCTAssertTrue([val isEqualToString:@"testdata"], @"Expected to get testdata back"); + ready = YES; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testNameAtRootAndNonRootLocations { + FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:self.databaseURL]; + XCTAssertTrue(ref.key == nil, @"Root key should be nil"); + FIRDatabaseReference * child = [ref child:@"a"]; + XCTAssertTrue([child.key isEqualToString:@"a"], @"Should be 'a'"); + FIRDatabaseReference * deeperChild = [child child:@"b/c"]; + XCTAssertTrue([deeperChild.key isEqualToString:@"c"], @"Should be 'c'"); +} + +- (void) testNameAndRefOnSnapshotsForRootAndNonRootLocations { + FIRDatabaseReference * ref = [[FIRDatabase database] reference]; + + __block BOOL ready = NO; + [ref removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue(snapshot.key == nil, @"Root snap should not have a key"); + NSString *snapString = [snapshot.ref description]; + XCTAssertTrue([snapString isEqualToString:snapString], @"Refs should be equivalent"); + FIRDataSnapshot *childSnap = [snapshot childSnapshotForPath:@"a"]; + XCTAssertTrue([childSnap.key isEqualToString:@"a"], @"Properly keys children"); + FIRDatabaseReference *childRef = [ref child:@"a"]; + NSString *refString = [childRef description]; + snapString = [childSnap.ref description]; + XCTAssertTrue([refString isEqualToString:snapString], @"Refs should be equivalent"); + childSnap = [childSnap childSnapshotForPath:@"b/c"]; + childRef = [childRef child:@"b/c"]; + XCTAssertTrue([childSnap.key isEqualToString:@"c"], @"properly keys children"); + refString = [childRef description]; + snapString = [childSnap.ref description]; + XCTAssertTrue([refString isEqualToString:snapString], @"Refs should be equivalent"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + // generate value event at root + [ref setValue:@"foo"]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testParentForRootAndNonRootLocations { + FIRDatabaseReference * ref = [[FIRDatabase database] reference]; + + XCTAssertTrue(ref.parent == nil, @"Parent of root should be nil"); + + FIRDatabaseReference * child = [ref child:@"a"]; + XCTAssertTrue([[child.parent description] isEqualToString:[ref description]], @"Should be equivalent locations"); + child = [ref child:@"a/b/c"]; + XCTAssertTrue([[child.parent.parent.parent description] isEqualToString:[ref description]], @"Should be equivalent locations"); +} + +- (void) testSettingNumericKeysConvertsToStrings { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSDictionary* toSet = @{@4: @"hi", @5: @"test"}; + + XCTAssertThrows([ref setValue:toSet], @"Keys must be strings"); +} + +- (void) testSetChildAndListenAtRootRegressionTest { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL ready = NO; + [writer removeValue]; + [[writer child:@"foo"] setValue:@"hi" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"foo" : @"hi"}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got child"); + ready = YES; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + + +- (void) testAccessingInvalidPathsThrows { + NSArray* badPaths = @[ + @".test", + @"test.", + @"fo$o", + @"[what", + @"ever]", + @"ha#sh" + ]; + + for (NSString* key in badPaths) { + NSString* url = [NSString stringWithFormat:@"%@/%@", self.databaseURL, key]; + XCTAssertThrows(^{ + FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:url]; + XCTFail(@"Should not get here with ref: %@", ref); + }(), @"should throw"); + url = [NSString stringWithFormat:@"%@/TESTS/%@", self.databaseURL, key]; + XCTAssertThrows(^{ + FIRDatabaseReference * ref = [[FIRDatabase database] referenceFromURL:url]; + XCTFail(@"Should not get here with ref: %@", ref); + }(), @"should throw"); + } + + __block BOOL ready = NO; + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (NSString *key in badPaths) { + XCTAssertThrows([snapshot childSnapshotForPath:key], @"should throw"); + XCTAssertThrows([snapshot hasChild:key], @"should throw"); + } + ready = YES; + }]; + [ref setValue:nil]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testSettingObjectsAtInvalidKeysThrow { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSArray* badPaths = @[ + @".test", + @"test.", + @"fo$o", + @"[what", + @"ever]", + @"ha#sh", + @"/thing", + @"th/ing", + @"thing/" + ]; + NSMutableArray* badObjs = [[NSMutableArray alloc] init]; + for (NSString* key in badPaths) { + [badObjs addObject:@{key: @"test"}]; + [badObjs addObject:@{@"deeper": @{key: @"test"}}]; + } + + for (NSDictionary* badObj in badObjs) { + XCTAssertThrows([ref setValue:badObj], @"Should throw"); + XCTAssertThrows([ref setValue:badObj andPriority:@5], @"Should throw"); + XCTAssertThrows([ref onDisconnectSetValue:badObj], @"Should throw"); + XCTAssertThrows([ref onDisconnectSetValue:badObj andPriority:@5], @"Should throw"); + // XXX transaction + } +} + +- (void) testSettingInvalidObjectsThrow { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + XCTAssertThrows([ref setValue:[NSDate date]], @"Should throw"); + + NSDictionary *data = @{@"invalid":@"data", @".sv":@"timestamp"}; + XCTAssertThrows([ref setValue:data], @"Should throw"); + + data = @{@".value": @{}}; + XCTAssertThrows([ref setValue:data], @"Should throw"); +} + +- (void) testInvalidUpdateThrow { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSArray *badUpdates = @[ + @{@"/":@"t", @"a":@"t"}, + @{@"a":@"t", @"a/b":@"t"}, + @{@"/a":@"t", @"a/b":@"t"}, + @{@"/a/b":@"t", @"a":@"t"}, + @{@"/a/b/.priority":@"t", @"/a/b":@"t"}, + @{@"/a/b/.sv":@"timestamp"}, + @{@"/a/b/.value":@"t"}, + @{@"/a/b/.priority":@{@"x": @"y"}}]; + + for (NSDictionary* update in badUpdates) { + XCTAssertThrows([ref updateChildValues:update], @"Should throw"); + XCTAssertThrows([ref onDisconnectUpdateChildValues:update], @"Should throw"); + } +} + +- (void) testSettingNull { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + XCTAssertNoThrow([ref setValue:nil], @"Should not throw"); + XCTAssertNoThrow([ref setValue:[NSNull null]], @"Should not throw"); +} + +- (void) testSettingNaN { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + XCTAssertThrows([ref setValue:[NSDecimalNumber notANumber]], @"Should throw"); +} + +- (void) testSettingInvalidPriority { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + XCTAssertThrows([ref setValue:@"3" andPriority:[NSDecimalNumber notANumber]], @"Should throw"); + XCTAssertThrows([ref setValue:@"4" andPriority:@{}], @"Should throw"); + XCTAssertThrows([ref setValue:@"5" andPriority:@[]], @"Should throw"); +} + +- (void) testRemoveFromOnMobileGraffitiBugAtAngelHack { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + + [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [[node child:[snapshot key]] removeValueWithCompletionBlock:^(NSError *err, FIRDatabaseReference *ref) { + done = YES; + }]; + }]; + + [[node childByAutoId] setValue:@"moo"]; + + [self waitUntil:^BOOL{ + return done; + }]; + + XCTAssertTrue(done, @"Properly finished"); +} + +- (void) testSetANodeWithAQuotedKey { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + __block FIRDataSnapshot * snap; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [node setValue:@{@"\"herp\"": @1234} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + done = YES; + XCTAssertEqualObjects(@1234, [[snap childSnapshotForPath:@"\"herp\""] value], @"Got it back"); + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + XCTAssertTrue(done, @"Properly finished"); +} + +- (void) testSetANodeWithASingleQuoteKey { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + __block FIRDataSnapshot * snap; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [node setValue:@{@"\"": @1234} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + done = YES; + XCTAssertEqualObjects(@1234, [[snap childSnapshotForPath:@"\""] value], @"Got it back"); + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + XCTAssertTrue(done, @"Properly finished"); +} + +- (void) testEmptyChildGetValueEventBeforeParent { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSArray* lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa/aaa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a/aa"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + ]; + + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:lookingFor]; + + [node setValue:@{@"b": @5}]; + + [et wait]; + +} + +// iOS behavior is different from what the recursive set test looks for. We don't raise events synchronously + +- (void) testOnAfterSetWaitsForLatestData { + // We test here that we don't cache sets, but they would be persisted so make sure we are running without + // persistence + FTupleFirebase* refs = [FTestHelpers getRandomNodePairWithoutPersistence]; + FIRDatabaseReference * node1 = refs.one; + FIRDatabaseReference * node2 = refs.two; + + __block BOOL ready = NO; + [node1 setValue:@5 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + [node2 setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + + [node1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSNumber *val = [snapshot value]; + XCTAssertTrue([val isEqualToNumber:@42], @"Should not have cached earlier set"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testOnceWaitsForLatestData { + // Can't tolerate stale data; so disable persistence. + FTupleFirebase* refs = [FTestHelpers getRandomNodePairWithoutPersistence]; + FIRDatabaseReference * node1 = refs.one; + FIRDatabaseReference * node2 = refs.two; + + __block BOOL ready = NO; + + [node1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + XCTAssertTrue([NSNull null] == val, @"First value should be null"); + + [node2 setValue:@5 withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + [node1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSNumber *val = [snapshot value]; + XCTAssertTrue([val isKindOfClass:[NSNumber class]] && [val isEqualToNumber:@5], @"Should get first value"); + ready = YES; + }]; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [node2 setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + [node1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSNumber *val = [snapshot value]; + XCTAssertTrue([val isEqualToNumber:@42], @"Got second number"); + ready = YES; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testMemoryFreeingOnUnlistenDoesNotCorruptData { + // Can't tolerate stale data; so disable persistence. + FTupleFirebase* refs = [FTestHelpers getRandomNodePairWithoutPersistence]; + FIRDatabaseReference * node2 = [[refs.one root] childByAutoId]; + + __block BOOL hasRun = NO; + __block BOOL ready = NO; + FIRDatabaseHandle handle1 = [refs.one observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (!hasRun) { + hasRun = YES; + id val = [snapshot value]; + XCTAssertTrue([NSNull null] == val, @"First time should be null"); + [refs.one setValue:@"test" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + ready = YES; + }]; + } + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [refs.one removeObserverWithHandle:handle1]; + + ready = NO; + [node2 setValue:@"hello" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + [refs.one observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSString *val = [snapshot value]; + XCTAssertTrue([val isEqualToString:@"test"], @"Get back the value we set above"); + [refs.two observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSString *val = [snapshot value]; + XCTAssertTrue([val isEqualToString:@"test"], @"Get back the value we set above"); + ready = YES; + }]; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + //write {x: 1, y : {t: 2, u: 3}} + //Listen at /. Then listen at /x/t + //unlisten at /y/t. Off at /. Once at /. Ensure data is still all there. + //Once at /y. Ensure data is still all there. + refs = [FTestHelpers getRandomNodePairWithoutPersistence]; + + ready = NO; + __block FIRDatabaseHandle deeplisten = NSNotFound; + __block FIRDatabaseHandle slashlisten = NSNotFound; + __weak FIRDatabaseReference * refOne = refs.one; + [refs.one setValue:@{@"x": @1, @"y": @{@"t": @2, @"u": @3}} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + slashlisten = [refOne observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + deeplisten = [[refOne child:@"y/t"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [[refOne child:@"y/t"] removeObserverWithHandle:deeplisten]; + [refOne removeObserverWithHandle:slashlisten]; + ready = YES; + }]; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [[refs.one child:@"x"] setValue:@"test" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + [refs.one observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"x" : @"test", @"y" : @{@"t" : @2, @"u" : @3}}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got the final value"); + ready = YES; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testUpdateRaisesCorrectLocalEvents { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot * snap = nil; + [node observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + __block BOOL ready = NO; + [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FEventTester* et = [[FEventTester alloc] initFrom:self]; + NSArray* expectations = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node child:@"d"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"d"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:expectations]; + + [et waitForInitialization]; + + [node updateChildValues:@{@"a": @4, @"d": @1}]; + + [et wait]; +} + +- (void) testUpdateRaisesCorrectRemoteEvents { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block BOOL ready = NO; + [writer setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FEventTester* et = [[FEventTester alloc] initFrom:self]; + NSArray* expectations = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[reader child:@"d"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildChanged withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeChildChanged withString:@"d"], + [[FTupleEventTypeString alloc] initWithFirebase:reader withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:expectations]; + + [et waitForInitialization]; + + [writer updateChildValues:@{@"a": @4, @"d": @1}]; + + [et wait]; + + ready = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *result = [snapshot value]; + NSDictionary *expected = @{@"a" : @4, @"b" : @2, @"c" : @3, @"d" : @1}; + XCTAssertTrue([result isEqualToDictionary:expected], @"Got expected results"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testUpdateChangesAreStoredCorrectlyByTheServer { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + [self waitForCompletionOf:writer setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}]; + + [self waitForCompletionOf:writer updateChildValues:@{@"a": @42}]; + + [self snapWaiter:reader withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary* result = [snapshot value]; + NSDictionary* expected = @{@"a": @42, @"b": @2, @"c": @3, @"d": @4}; + XCTAssertTrue([result isEqualToDictionary:expected], @"Expected updated value"); + }]; +} + +- (void) testUpdateDoesntAffectPriorityLocally { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot * snap = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} andPriority:@"testpri"]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap priority] isEqualToString:@"testpri"], @"Got initial priority"); + snap = nil; + + [ref updateChildValues:@{@"a": @4}]; + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap priority] isEqualToString:@"testpri"], @"Got initial priority"); +} + +- (void) testUpdateDoesntAffectPriorityRemotely { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block BOOL ready = NO; + [writer setValue:@{@"a": @1, @"b": @2, @"c": @3} andPriority:@"testpri" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSString *result = [snapshot priority]; + XCTAssertTrue([result isEqualToString:@"testpri"], @"Expected initial priority"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [writer updateChildValues:@{@"a": @4} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSString *result = [snapshot priority]; + XCTAssertTrue([result isEqualToString:@"testpri"], @"Expected initial priority"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testUpdateReplacesChildrenAndIsNotRecursive { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block FIRDataSnapshot * localSnap = nil; + __block BOOL ready = NO; + + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + localSnap = snapshot; + }]; + + [writer setValue:@{@"a": @{@"aa": @1, @"ab": @2}}]; + [writer updateChildValues:@{@"a": @{@"aa": @1}} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + + [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *result = [snapshot value]; + NSDictionary *expected = @{@"a" : @{@"aa" : @1}}; + XCTAssertTrue([result isEqualToDictionary:expected], @"Should get new value"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + NSDictionary* result = [localSnap value]; + NSDictionary* expected = @{@"a": @{@"aa": @1}}; + return ready && [result isEqualToDictionary:expected]; + }]; +} + +- (void) testDeepUpdatesWork { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block FIRDataSnapshot * localSnap = nil; + __block BOOL ready = NO; + + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + localSnap = snapshot; + }]; + + [writer setValue:@{@"a": @{@"aa": @1, @"ab": @2}}]; + [writer updateChildValues:@{@"a/aa": @10, + @".priority": @3.0, + @"a/ab": @{@".priority": @2.0, + @".value": @20}} + withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + ready = NO; + + [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *result = [snapshot value]; + NSDictionary *expected = @{@"a" : @{@"aa" : @10, @"ab" : @20}}; + XCTAssertTrue([result isEqualToDictionary:expected], @"Should get new value"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + NSDictionary* result = [localSnap value]; + NSDictionary* expected = @{@"a": @{@"aa": @10, @"ab": @20}}; + return ready && [result isEqualToDictionary:expected]; + }]; +} + +// Type signature means we don't need a test for updating scalars. They wouldn't compile + +- (void) testEmptyUpdateWorks { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref updateChildValues:@{} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + XCTAssertTrue(error == nil, @"Should not be an error"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +// XXX update stress test + +- (void) testUpdateFiresCorrectEventWhenAChildIsDeleted { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block FIRDataSnapshot * localSnap = nil; + __block FIRDataSnapshot * remoteSnap = nil; + + [self waitForCompletionOf:writer setValue:@{@"a": @12, @"b": @6}]; + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + localSnap = snapshot; + }]; + + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + remoteSnap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + localSnap = nil; + remoteSnap = nil; + + [writer updateChildValues:@{@"a": [NSNull null]}]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + NSDictionary* expected = @{@"b": @6}; + XCTAssertTrue([[remoteSnap value] isEqualToDictionary:expected], @"Removed child"); + XCTAssertTrue([[localSnap value] isEqualToDictionary:expected], @"Removed child"); +} + +- (void) testUpdateFiresCorrectEventOnNewChildren { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block FIRDataSnapshot * localSnap = nil; + __block FIRDataSnapshot * remoteSnap = nil; + + [[writer child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + localSnap = snapshot; + }]; + + [[reader child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + remoteSnap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + localSnap = nil; + remoteSnap = nil; + + [writer updateChildValues:@{@"a": @42}]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + XCTAssertTrue([[remoteSnap value] isEqualToNumber:@42], @"Added child"); + XCTAssertTrue([[localSnap value] isEqualToNumber:@42], @"Added child"); +} + +- (void) testUpdateFiresCorrectEventOnDeletedChildren { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block FIRDataSnapshot * localSnap = nil; + __block FIRDataSnapshot * remoteSnap = nil; + [self waitForCompletionOf:writer setValue:@{@"a": @12}]; + [[writer child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + localSnap = snapshot; + }]; + + [[reader child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + remoteSnap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + localSnap = nil; + remoteSnap = nil; + + [writer updateChildValues:@{@"a": [NSNull null]}]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + XCTAssertTrue([remoteSnap value] == [NSNull null], @"Removed child"); + XCTAssertTrue([localSnap value] == [NSNull null], @"Removed child"); +} + +- (void) testUpdateFiresCorrectEventOnChangedChildren { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + [self waitForCompletionOf:writer setValue:@{@"a": @12}]; + + __block FIRDataSnapshot * localSnap = nil; + __block FIRDataSnapshot * remoteSnap = nil; + + [[writer child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + localSnap = snapshot; + }]; + + [[reader child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + remoteSnap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + localSnap = nil; + remoteSnap = nil; + + [self waitForCompletionOf:writer updateChildValues:@{@"a": @11}]; + + [self waitUntil:^BOOL{ + return localSnap != nil && remoteSnap != nil; + }]; + + XCTAssertTrue([[remoteSnap value] isEqualToNumber:@11], @"Changed child"); + XCTAssertTrue([[localSnap value] isEqualToNumber:@11], @"Changed child"); +} + + +- (void) testUpdateOfPriorityWorks { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + FIRDatabaseReference * writer = refs.two; + + __block BOOL ready = NO; + [writer setValue:@{@"a": @5, @".priority": @"pri1"}]; + [writer updateChildValues:@{@"a": @6, @".priority": @"pri2", @"b": @{ @".priority": @"pri3", @"c": @10 } } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + NSLog(@"error? %@", error); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqualObjects([[snapshot childSnapshotForPath:@"a"] value], @6, @"Should match write values"); + XCTAssertTrue([[snapshot priority] isEqualToString:@"pri2"], @"Should get updated priority"); + XCTAssertTrue([[[snapshot childSnapshotForPath:@"b"] priority] isEqualToString:@"pri3"], @"Should get updated priority"); + XCTAssertEqualObjects([[snapshot childSnapshotForPath:@"b/c"] value], @10, @"Should match write values"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testSetWithCircularReferenceFails { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSMutableDictionary* toSet = [[NSMutableDictionary alloc] init]; + NSDictionary* lol = @{@"foo": @"bar", @"circular": toSet}; + [toSet setObject:lol forKey:@"lol"]; + + XCTAssertThrows([ref setValue:toSet], @"Should not be able to set circular dictionary"); +} + +- (void) testLargeNumbers { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + long long jsMaxInt = 9007199254740992; + long jsMaxIntPlusOne = jsMaxInt + 1; + NSNumber* toSet = [NSNumber numberWithLong:jsMaxIntPlusOne]; + [ref setValue:toSet]; + + __block FIRDataSnapshot * snap = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + NSNumber* result = [snap value]; + XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number"); + + toSet = [NSNumber numberWithLong:LONG_MAX]; + snap = nil; + + [ref setValue:toSet]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + result = [snap value]; + XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number"); + + snap = nil; + toSet = [NSNumber numberWithDouble:DBL_MAX]; + [ref setValue:toSet]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + result = [snap value]; + XCTAssertTrue([result isEqualToNumber:toSet], @"Should get back same number"); +} + +- (void) testParentDeleteShadowsChildListeners { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * deleter = refs.two; + + NSString* childName = [writer childByAutoId].key; + + __block BOOL called = NO; + [[deleter child:childName] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertFalse(called, @"Should only be hit once"); + called = YES; + XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null"); + }]; + + WAIT_FOR(called); + + __block BOOL done = NO; + [[writer child:childName] setValue:@"foo"]; + [deleter removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + WAIT_FOR(done); +} + +- (void) testParentDeleteShadowsChildListenersWithNonDefaultQuery { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * deleter = refs.two; + + NSString* childName = [writer childByAutoId].key; + + __block BOOL queryCalled = NO; + __block BOOL deepChildCalled = NO; + [[[[deleter child:childName] queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertFalse(queryCalled, @"Should only be hit once"); + queryCalled = YES; + XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null"); + }]; + + [[[deleter child:childName] child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertFalse(deepChildCalled, @"Should only be hit once"); + deepChildCalled = YES; + XCTAssertTrue(snapshot.value == [NSNull null], @"Value should be null"); + }]; + + WAIT_FOR(deepChildCalled && queryCalled); + + __block BOOL done = NO; + [[writer child:childName] setValue:@"foo"]; + [deleter removeValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + WAIT_FOR(done); +} + +- (void) testLocalServerValuesEventuallyButNotImmediatelyMatchServer { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference* writer = refs.one; + FIRDatabaseReference* reader = refs.two; + __block int done = 0; + + NSMutableArray* readSnaps = [[NSMutableArray alloc] init]; + NSMutableArray* writeSnaps = [[NSMutableArray alloc] init]; + + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot value] != [NSNull null]) { + [readSnaps addObject:snapshot]; + if (readSnaps.count == 1) { + done += 1; + } + } + }]; + + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot value] != [NSNull null]) { + [writeSnaps addObject:snapshot]; + if (writeSnaps.count == 2) { + done += 1; + } + } + }]; + + [writer setValue:[FIRServerValue timestamp] andPriority:[FIRServerValue timestamp]]; + + [self waitUntil:^BOOL{ + return done == 2; + }]; + + XCTAssertEqual((unsigned long)[readSnaps count], (unsigned long)1, @"Should have received one snapshot on reader"); + XCTAssertEqual((unsigned long)[writeSnaps count], (unsigned long)2, @"Should have received two snapshots on writer"); + + FIRDataSnapshot * firstReadSnap = [readSnaps objectAtIndex:0]; + FIRDataSnapshot * firstWriteSnap = [writeSnaps objectAtIndex:0]; + FIRDataSnapshot * secondWriteSnap = [writeSnaps objectAtIndex:1]; + + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + XCTAssertTrue([now doubleValue] - [firstWriteSnap.value doubleValue] < 3000, @"Should have received a local event with a value close to timestamp"); + XCTAssertTrue([now doubleValue] - [firstWriteSnap.priority doubleValue] < 3000, @"Should have received a local event with a priority close to timestamp"); + XCTAssertTrue([now doubleValue] - [secondWriteSnap.value doubleValue] < 3000, @"Should have received a server event with a value close to timestamp"); + XCTAssertTrue([now doubleValue] - [secondWriteSnap.priority doubleValue] < 3000, @"Should have received a server event with a priority close to timestamp"); + + XCTAssertFalse([firstWriteSnap value] == [secondWriteSnap value], @"Initial and future writer values should be different"); + XCTAssertFalse([firstWriteSnap priority] == [secondWriteSnap priority], @"Initial and future writer priorities should be different"); + XCTAssertEqualObjects(firstReadSnap.value, secondWriteSnap.value, @"Eventual reader and writer values should be equal"); + XCTAssertEqualObjects(firstReadSnap.priority, secondWriteSnap.priority, @"Eventual reader and writer priorities should be equal"); +} + +- (void) testServerValuesSetWithPriorityRemoteEvents { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + NSDictionary* data = @{ + @"a": [FIRServerValue timestamp], + @"b": @{ + @".value": [FIRServerValue timestamp], + @".priority": [FIRServerValue timestamp] + } + }; + + __block BOOL done = NO; + [writer setValue:data andPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [self snapWaiter:reader withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary* value = [snapshot value]; + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + NSNumber* timestamp = [snapshot priority]; + XCTAssertTrue([[snapshot priority] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); + XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"a"], @"Should get back matching ServerValue.TIMESTAMP"); + XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"b"], @"Should get back matching ServerValue.TIMESTAMP"); + XCTAssertEqualObjects([snapshot priority], [[snapshot childSnapshotForPath:@"b"] priority], @"Should get back matching ServerValue.TIMESTAMP"); + }]; +} + +- (void) testServerValuesSetPriorityRemoteEvents { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block FIRDataSnapshot *snap = nil; + [reader observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [self waitForCompletionOf:[writer child:@"a"] setValue:@1 andPriority:nil]; + [self waitForCompletionOf:[writer child:@"b"] setValue:@1 andPriority:@1]; + [self waitForValueOf:[reader child:@"a"] toBe:@1]; + + __block BOOL done = NO; + [[writer child:@"a"] setPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done && snap != nil; + }]; + + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + NSNumber* timestamp = [snap priority]; + XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); +} + +- (void) testServerValuesUpdateRemoteEvents { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block FIRDataSnapshot *snap = nil; + __block BOOL done = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + if (snap && [[snap childSnapshotForPath:@"a/b/d"] value] != [NSNull null]) { + done = YES; + } + }]; + + [[writer child:@"a/b/c"] setValue:@1]; + [[writer child:@"a"] updateChildValues:@{ @"b": @{ @"c": [FIRServerValue timestamp], @"d":@1 } }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + NSNumber* timestamp = [[snap childSnapshotForPath:@"a/b/c"] value]; + XCTAssertTrue([[[snap childSnapshotForPath:@"a/b/c"] value] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); +} + +- (void) testServerValuesSetWithPriorityLocalEvents { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSDictionary* data = @{ + @"a": [FIRServerValue timestamp], + @"b": @{ + @".value": [FIRServerValue timestamp], + @".priority": [FIRServerValue timestamp] + } + }; + + __block FIRDataSnapshot *snap = nil; + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + __block BOOL done = NO; + [node setValue:data andPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { done = YES; }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [self snapWaiter:node withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary* value = [snapshot value]; + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + NSNumber* timestamp = [snapshot priority]; + XCTAssertTrue([[snapshot priority] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); + XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"a"], @"Should get back matching ServerValue.TIMESTAMP"); + XCTAssertEqualObjects([snapshot priority], [value objectForKey:@"b"], @"Should get back matching ServerValue.TIMESTAMP"); + XCTAssertEqualObjects([snapshot priority], [[snapshot childSnapshotForPath:@"b"] priority], @"Should get back matching ServerValue.TIMESTAMP"); + }]; +} + +- (void) testServerValuesSetPriorityLocalEvents { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot *snap = nil; + [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + __block BOOL done = NO; + + [[node child:@"a"] setValue:@1 andPriority:nil]; + [[node child:@"b"] setValue:@1 andPriority:@1]; + [[node child:@"a"] setPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + NSNumber* timestamp = [snap priority]; + XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); +} + +- (void) testServerValuesUpdateLocalEvents { + FIRDatabaseReference * node1 = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot *snap1 = nil; + [node1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap1 = snapshot; + }]; + + __block FIRDataSnapshot *snap2 = nil; + [node1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap2 = snapshot; + }]; + + [node1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:[FIRServerValue timestamp]]; + return [FIRTransactionResult successWithValue:currentData]; + }]; + + [self waitUntil:^BOOL{ + return snap1 != nil && snap2 != nil && [snap1 value] != nil && [snap2 value] != nil; + }]; + + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + + NSNumber* timestamp1 = [snap1 value]; + XCTAssertTrue([[snap1 value] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp1 doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); + + NSNumber* timestamp2 = [snap2 value]; + XCTAssertTrue([[snap2 value] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp2 doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); +} + +- (void) testServerValuesTransactionLocalEvents { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot *snap = nil; + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [[node child:@"a/b/c"] setValue:@1]; + [[node child:@"a"] updateChildValues:@{ @"b": @{ @"c": [FIRServerValue timestamp], @"d":@1 } }]; + + [self waitUntil:^BOOL{ + return snap != nil && [[snap childSnapshotForPath:@"a/b/d"] value] != nil; + }]; + + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + NSNumber* timestamp = [[snap childSnapshotForPath:@"a/b/c"] value]; + XCTAssertTrue([[[snap childSnapshotForPath:@"a/b/c"] value] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); +} + +- (void) testUpdateAfterChildSet { + FIRDatabaseReference *node = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + __weak FIRDatabaseReference *weakRef = node; + [node setValue:@{@"a": @"a"} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + [weakRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (snapshot.childrenCount == 3 && [snapshot hasChild:@"a"] && [snapshot hasChild:@"b"] && [snapshot hasChild:@"c"]) { + done = YES; + } + }]; + + [[weakRef child:@"b"] setValue:@"b"]; + + [weakRef updateChildValues:@{@"c" : @"c"}]; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testDeltaSyncNoDataUpdatesAfterReconnect { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"]; + FIRDatabaseReference * ref2 = [[[FIRDatabaseReference alloc] initWithConfig:cfg] child:ref.key]; + __block id data = @{ @"a": @1, @"b": @2, @"c": @{ @".priority": @3, @".value": @3}, @"d": @4 }; + [self waitForCompletionOf:ref setValue:data]; + + __block BOOL gotData = NO; + [ref2 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertFalse(gotData, @"event triggered twice."); + gotData = YES; + XCTAssertEqualObjects(snapshot.valueInExportFormat, data, @"Got wrong data."); + }]; + + [self waitUntil:^BOOL{ return gotData; }]; + + __block BOOL done = NO; + XCTAssertEqual(ref2.repo.dataUpdateCount, 1L, @"Should have gotten one update."); + + // Bounce connection + [FRepoManager interrupt:cfg]; + [FRepoManager resume:cfg]; + + [[[ref2 root] child:@".info/connected"] observeEventType:FIRDataEventTypeValue + withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot.value boolValue]) { + // We're connected. Do one more round-trip to make sure all state restoration is done + [[[ref2 root] child:@"foobar/empty/blah"] setValue:nil withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(ref2.repo.dataUpdateCount, 1L, @"Should have gotten one update."); + done = YES; + }]; + } + } + ]; + + [self waitUntil:^BOOL{ return done; }]; + + // cleanup + [FRepoManager interrupt:cfg]; + [FRepoManager disposeRepos:cfg]; +} + +- (void) testServerValuesEventualConsistencyBetweenLocalAndRemote { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block FIRDataSnapshot *writerSnap = nil; + __block FIRDataSnapshot *readerSnap = nil; + + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + readerSnap = snapshot; + }]; + + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + writerSnap = snapshot; + }]; + + [writer setValue:[FIRServerValue timestamp] andPriority:[FIRServerValue timestamp]]; + + [self waitUntil:^BOOL{ + if (readerSnap && writerSnap && [[readerSnap value] isKindOfClass:[NSNumber class]] && [[writerSnap value] isKindOfClass:[NSNumber class]]) { + if ([[readerSnap value] doubleValue] == [[writerSnap value] doubleValue]) { + return YES; + } + } + return NO; + }]; +} + +// Listens at a location and then creates a bunch of children, waiting for them all to complete. +- (void) testChildAddedPerf1 { + if (!runPerfTests) return; + + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + [ref observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + }]; + + NSDate *start = [NSDate date]; + int COUNT = 1000; + __block BOOL done = NO; + __block NSDate *finished = nil; + for(int i = 0; i < COUNT; i++) { + [[ref childByAutoId] setValue:@"01234567890123456789012345678901234567890123456789" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (i == (COUNT - 1)) { + finished = [NSDate date]; + done = YES; + } + }]; + } + [self waitUntil:^BOOL { + return done; + } timeout:300]; + NSTimeInterval elapsed = [finished timeIntervalSinceDate:start]; + NSLog(@"Elapsed: %f", elapsed); +} + +// Listens at a location, then adds a bunch of grandchildren under a single child. +- (void) testDeepChildAddedPerf1 { + if (!runPerfTests) return; + + FIRDatabaseReference *ref = [FTestHelpers getRandomNode], + *childRef = [ref child:@"child"]; + + [ref observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + }]; + + NSDate *start = [NSDate date]; + int COUNT = 1000; + __block BOOL done = NO; + __block NSDate *finished = nil; + for(int i = 0; i < COUNT; i++) { + [[childRef childByAutoId] setValue:@"01234567890123456789012345678901234567890123456789" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (i == (COUNT - 1)) { + finished = [NSDate date]; + done = YES; + } + }]; + } + [self waitUntil:^BOOL { + return done; + } timeout:300]; + + NSTimeInterval elapsed = [finished timeIntervalSinceDate:start]; + NSLog(@"Elapsed: %f", elapsed); +} + +// Listens at a location, then adds a bunch of grandchildren under a single child, but does it with merges. +// NOTE[2015-07-14]: This test is still pretty slow, because [FWriteTree removeWriteId] ends up rebuilding the tree after +// every ack. +- (void) testDeepChildAddedPerfViaMerge1 { + if (!runPerfTests) return; + + FIRDatabaseReference *ref = [FTestHelpers getRandomNode], + *childRef = [ref child:@"child"]; + + [ref observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + }]; + + NSDate *start = [NSDate date]; + int COUNT = 250; + __block BOOL done = NO; + __block NSDate *finished = nil; + for(int i = 0; i < COUNT; i++) { + NSString *childName = [childRef childByAutoId].key; + [childRef updateChildValues:@{ + childName: @"01234567890123456789012345678901234567890123456789" + } withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (i == (COUNT - 1)) { + finished = [NSDate date]; + done = YES; + } + }]; + } + [self waitUntil:^BOOL { + return done; + } timeout:300]; + + NSTimeInterval elapsed = [finished timeIntervalSinceDate:start]; + NSLog(@"Elapsed: %f", elapsed); +} + +@end diff --git a/Example/Database/Tests/Integration/FDotInfo.h b/Example/Database/Tests/Integration/FDotInfo.h new file mode 100644 index 0000000..73bd4c7 --- /dev/null +++ b/Example/Database/Tests/Integration/FDotInfo.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestBase.h" + +@interface FDotInfo : FTestBase + +@end diff --git a/Example/Database/Tests/Integration/FDotInfo.m b/Example/Database/Tests/Integration/FDotInfo.m new file mode 100644 index 0000000..0245dc5 --- /dev/null +++ b/Example/Database/Tests/Integration/FDotInfo.m @@ -0,0 +1,173 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FDotInfo.h" +#import "FTestHelpers.h" +#import "FIRDatabaseConfig_Private.h" + +@implementation FDotInfo + +- (void) testCanGetReferenceToInfoNodes { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + [ref.root child:@".info"]; + [ref.root child:@".info/foo"]; +} + +- (void) testCantWriteToInfo { + FIRDatabaseReference * ref = [[FTestHelpers getRandomNode].root child:@".info"]; + XCTAssertThrows([ref setValue:@"hi"], @"Cannot write to path at /.info"); + XCTAssertThrows([ref setValue:@"hi" andPriority:@5], @"Cannot write to path at /.info"); + XCTAssertThrows([ref setPriority:@"hi"], @"Cannot write to path at /.info"); + XCTAssertThrows([ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult successWithValue:currentData]; + }], @"Cannot write to path at /.info"); + XCTAssertThrows([ref removeValue], @"Cannot write to path at /.info"); + XCTAssertThrows([[ref child:@"test"] setValue:@"hi"], @"Cannot write to path at /.info"); +} + +- (void) testCanWatchInfoConnected { + FIRDatabaseReference * rootRef = [FTestHelpers getRandomNode].root; + __block BOOL done = NO; + [[rootRef child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([[snapshot value] boolValue]) { + done = YES; + } + }]; + [self waitUntil:^{ return done; }]; +} + +- (void) testInfoConnectedGoesToFalseOnDisconnect { + FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"]; + FIRDatabaseReference * rootRef = [[FIRDatabaseReference alloc] initWithConfig:cfg]; + __block BOOL everConnected = NO; + __block NSMutableString *connectedHistory = [[NSMutableString alloc] init]; + [[rootRef child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([[snapshot value] boolValue]) { + everConnected = YES; + } + + if (everConnected) { + [connectedHistory appendString:([[snapshot value] boolValue] ? @"YES," : @"NO,")]; + } + }]; + [self waitUntil:^{ return everConnected; }]; + + [FRepoManager interrupt:cfg]; + [FRepoManager resume:cfg]; + + [self waitUntil:^BOOL{ + return [connectedHistory isEqualToString:@"YES,NO,YES,"]; + }]; + + [FRepoManager interrupt:cfg]; + [FRepoManager disposeRepos:cfg]; +} + +- (void) testInfoServerTimeOffset { + FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"]; + FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:cfg]; + + // make sure childByAutoId works + [ref childByAutoId]; + + NSMutableArray* offsets = [[NSMutableArray alloc] init]; + + [[ref child:@".info/serverTimeOffset"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSLog(@"got value: %@", snapshot.value); + [offsets addObject:snapshot.value]; + }]; + + WAIT_FOR(offsets.count == 1); + + XCTAssertTrue([[offsets objectAtIndex:0] isKindOfClass:[NSNumber class]], @"Second element should be a number, in milliseconds"); + + // make sure childByAutoId still works + [ref childByAutoId]; + + [FRepoManager interrupt:cfg]; + [FRepoManager disposeRepos:cfg]; +} + +- (void) testManualConnectionManagement { + FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"test-config"]; + FIRDatabaseConfig *altCfg = [FIRDatabaseConfig configForName:@"alt-config"]; + + FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithConfig:cfg]; + FIRDatabaseReference * refAlt = [[FIRDatabaseReference alloc] initWithConfig:altCfg]; + + // Wait until we're connected to both Firebases + __block BOOL ready = NO; + [[ref child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + ready = [[snapshot value] boolValue]; + }]; + [self waitUntil:^{ return ready; }]; + [[ref child:@".info/connected"] removeAllObservers]; + + ready = NO; + [[refAlt child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + ready = [[snapshot value] boolValue]; + }]; + [self waitUntil:^{ return ready; }]; + [[refAlt child:@".info/connected"] removeAllObservers]; + + [FIRDatabaseReference goOffline]; + + // Ensure we're disconnected from both Firebases + ready = NO; + + [[ref child:@".info/connected"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertFalse([[snapshot value] boolValue], @".info/connected should be false"); + ready = YES; + }]; + [self waitUntil:^{ return ready; }]; + ready = NO; + [[refAlt child:@".info/connected"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertFalse([[snapshot value] boolValue], @".info/connected should be false"); + ready = YES; + }]; + [self waitUntil:^{ return ready; }]; + + // Ensure that we don't automatically reconnect upon new Firebase creation + FIRDatabaseReference * refDup = [[FIRDatabaseReference alloc] initWithConfig:altCfg]; + [[refDup child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([[snapshot value] boolValue]) { + XCTFail(@".info/connected should remain false"); + } + }]; + + // Wait for 1.5 seconds to make sure connected remains false + [NSThread sleepForTimeInterval:1.5]; + [[refDup child:@".info/connected"] removeAllObservers]; + + [FIRDatabaseReference goOnline]; + + // Ensure we're reconnected to both Firebases + ready = NO; + [[ref child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + ready = [[snapshot value] boolValue]; + }]; + [self waitUntil:^{ return ready; }]; + [[ref child:@".info/connected"] removeAllObservers]; + + ready = NO; + [[refAlt child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + ready = [[snapshot value] boolValue]; + }]; + [self waitUntil:^{ return ready; }]; + [[refAlt child:@".info/connected"] removeAllObservers]; +} +@end diff --git a/Example/Database/Tests/Integration/FEventTests.h b/Example/Database/Tests/Integration/FEventTests.h new file mode 100644 index 0000000..8ea5eef --- /dev/null +++ b/Example/Database/Tests/Integration/FEventTests.h @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FTestBase.h" + +@interface FEventTests : FTestBase { + BOOL rl; +} + +@end diff --git a/Example/Database/Tests/Integration/FEventTests.m b/Example/Database/Tests/Integration/FEventTests.m new file mode 100644 index 0000000..8b11e9d --- /dev/null +++ b/Example/Database/Tests/Integration/FEventTests.m @@ -0,0 +1,506 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FEventTests.h" +#import "FTestHelpers.h" +#import "FTupleEventTypeString.h" +#import "FEventTester.h" + +@implementation FEventTests + + + +- (void) testInvalidEventType { + FIRDatabaseReference * f = [FTestHelpers getRandomNode]; + XCTAssertThrows([f observeEventType:-4 withBlock:^(FIRDataSnapshot *s) {}], @"Invalid event type properly throws an error"); +} + +- (void) testWriteLeafExpectValueChanged { + + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writeNode = tuple.one; + FIRDatabaseReference * readNode = tuple.two; + + __block BOOL done = NO; + [writeNode setValue:@1234 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; + + [super snapWaiter:readNode withBlock:^(FIRDataSnapshot *s) { + XCTAssertEqualObjects([s value], @1234, @"Proper value in snapshot"); + }]; +} + +- (void) testWRiteLeafNodeThenExpectValueEvent { + FIRDatabaseReference * writeNode = [FTestHelpers getRandomNode]; + [writeNode setValue:@42]; + + [super snapWaiter:writeNode withBlock:^(FIRDataSnapshot *s) { + XCTAssertEqualObjects([s value], @42, @"Proper value in snapshot"); + }]; + +} + +- (void) testWriteLeafNodeThenExpectChildAddedEventThenValueEvent { + + FIRDatabaseReference * writeNode = [FTestHelpers getRandomNode]; + + [[writeNode child:@"foo"] setValue:@878787]; + + NSArray* lookingFor = @[ + [[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeChildAdded withString:@"foo"], + [[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil], + ]; + + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:lookingFor]; + [et wait]; + + [super snapWaiter:writeNode withBlock:^(FIRDataSnapshot *s) { + XCTAssertEqualObjects([[s childSnapshotForPath:@"foo"] value], @878787, @"Got proper value"); + }]; + +} + +- (void) testWriteTwoNestedLeafNodesChange { + +} + +- (void) testSetMultipleEventListenersOnSameNode { + + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writeNode = tuple.one; + FIRDatabaseReference * readNode = tuple.two; + + [writeNode setValue:@42]; + + // two write nodes + FEventTester* et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil] ]]; + [et wait]; + + et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:writeNode withEvent:FIRDataEventTypeValue withString:nil] ]]; + [et wait]; + + // two read nodes + et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:readNode withEvent:FIRDataEventTypeValue withString:nil] ]]; + [et wait]; + + et = [[FEventTester alloc] initFrom:self]; + [et addLookingFor:@[[[FTupleEventTypeString alloc] initWithFirebase:readNode withEvent:FIRDataEventTypeValue withString:nil] ]]; + [et wait]; + +} + +- (void) testUnsubscribeEventsAndConfirmThatEventsNoLongerFire { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + __block int numValueCB = 0; + + FIRDatabaseHandle handle = [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) { + numValueCB = numValueCB + 1; + }]; + + // Set + for(int i = 0; i < 3; i++) { + [node setValue:[NSNumber numberWithInt:i]]; + } + + // bye + [node removeObserverWithHandle:handle]; + + // set again + for(int i = 10; i < 15; i++) { + [node setValue:[NSNumber numberWithInt:i]]; + } + + for(int i = 20; i < 25; i++) { + [node setValue:[NSNumber numberWithInt:i]]; + } + + // Should just be 3 + [self waitUntil:^BOOL{ + return numValueCB == 3; + }]; +} + +- (void) testCanWriteACompoundObjectAndGetMoreGranularEventsForIndividualChanges { + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writeNode = tuple.one; + FIRDatabaseReference * readNode = tuple.two; + + __block BOOL done = NO; + [writeNode setValue:@{@"a": @10, @"b": @20} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ return done; }]; + + NSArray* lookingForW = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil], + ]; + + NSArray* lookingForR = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil], + ]; + + FEventTester* etW = [[FEventTester alloc] initFrom:self]; + [etW addLookingFor:lookingForW]; + [etW wait]; + + FEventTester* etR = [[FEventTester alloc] initFrom:self]; + [etR addLookingFor:lookingForR]; + [etR wait]; + + // Modify compound but just change one of them + + lookingForW = @[[[FTupleEventTypeString alloc] initWithFirebase:[writeNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] ]; + lookingForR = @[[[FTupleEventTypeString alloc] initWithFirebase:[readNode child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] ]; + + [etW addLookingFor:lookingForW]; + [etR addLookingFor:lookingForR]; + + [writeNode setValue:@{@"a": @10, @"b": @30}]; + + [etW wait]; + [etR wait]; +} + + +- (void) testValueEventIsFiredForEmptyNode { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL valueFired = NO; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) { + XCTAssertTrue([[s value] isEqual:[NSNull null]], @"Value is properly nil"); + valueFired = YES; + }]; + + [self waitUntil:^BOOL{ + return valueFired; + }]; +} + +- (void) testCorrectEventsRaisedWhenLeafTurnsIntoInternalNode { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + NSMutableString* eventString = [[NSMutableString alloc] init]; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) { + if ([s hasChildren]) { + [eventString appendString:@", got children"]; + } + else { + [eventString appendFormat:@", value %@", [s value]]; + } + }]; + + [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *s) { + [eventString appendFormat:@", child_added %@", [s key]]; + }]; + + [node setValue:@42]; + [node setValue:@{@"a": @2}]; + [node setValue:@84]; + __block BOOL done = NO; + [node setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; + + XCTAssertEqualObjects(@", value 42, child_added a, got children, value 84, value <null>", eventString, @"Proper order seen"); +} + +- (void) testRegisteringCallbackMultipleTimesAndUnregistering { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + __block int changes = 0; + + fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) { changes = changes + 1; }; + + FIRDatabaseHandle handle1 = [node observeEventType:FIRDataEventTypeValue withBlock:cb]; + FIRDatabaseHandle handle2 = [node observeEventType:FIRDataEventTypeValue withBlock:cb]; + FIRDatabaseHandle handle3 = [node observeEventType:FIRDataEventTypeValue withBlock:cb]; + + __block BOOL done = NO; + + [node setValue:@42 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; + done = NO; + + XCTAssertTrue(changes == 3, @"Saw 3 callback events %d", changes); + + [node removeObserverWithHandle:handle1]; + [node setValue:@84 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; + done = NO; + + XCTAssertTrue(changes == 5, @"Saw 5 callback events %d", changes); + + [node removeObserverWithHandle:handle2]; + [node setValue:@168 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; + done = NO; + + XCTAssertTrue(changes == 6, @"Saw 6 callback events %d", changes); + + [node removeObserverWithHandle:handle3]; + [node setValue:@376 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; + done = NO; + + XCTAssertTrue(changes == 6, @"Saw 6 callback events %d", changes); + + NSLog(@"callbacks: %d", changes); + +} + +- (void) testUnregisteringTheSameCallbackTooManyTimesDoesNothing { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) { }; + + FIRDatabaseHandle handle1 = [node observeEventType:FIRDataEventTypeValue withBlock:cb]; + [node removeObserverWithHandle:handle1]; + [node removeObserverWithHandle:handle1]; + + XCTAssertTrue(YES, @"Properly reached end of test without throwing errors."); +} + +- (void) testOnceValueFiresExactlyOnce { + FIRDatabaseReference * path = [FTestHelpers getRandomNode]; + __block BOOL firstCall = YES; + + [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue(firstCall, @"Properly saw first call"); + firstCall = NO; + XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value"); + }]; + + [path setValue:@42]; + [path setValue:@84]; + + __block BOOL done = NO; + + [path setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; +} + +- (void) testOnceChildAddedFiresExaclyOnce { + __block int badCount = 0; + + // for(int i = 0; i < 100; i++) { + + FIRDatabaseReference * path = [FTestHelpers getRandomNode]; + __block BOOL firstCall = YES; + + __block BOOL done = NO; + + + [path observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue(firstCall, @"Properly saw first call"); + firstCall = NO; + XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value"); + XCTAssertEqualObjects(@"foo", [snapshot key], @"Properly saw the first node"); + if (![[snapshot value] isEqual:@42]) { + exit(-1); + badCount = badCount + 1; + } + + done = YES; + + + }]; + + [[path child:@"foo"] setValue:@42]; + [[path child:@"bar"] setValue:@84]; // XXX FIXME sometimes this event fires first + [[path child:@"foo"] setValue:@168]; + + +// [path setValue:nil withCompletionBlock:^(BOOL status) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; + + + // } + + NSLog(@"BADCOUNT: %d", badCount); +} + +- (void) testOnceValueFiresExacltyOnceEvenIfThereIsASetInsideCallback { + FIRDatabaseReference * path = [FTestHelpers getRandomNode]; + __block BOOL firstCall = YES; + __block BOOL done = NO; + + [path observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue(firstCall, @"Properly saw first call"); + if (firstCall) { + firstCall = NO; + XCTAssertEqualObjects(@42, [snapshot value], @"Properly saw node value"); + [path setValue:@43]; + done = YES; + } + else { + XCTFail(@"Callback got called more than once."); + } + }]; + + [path setValue:@42]; + [path setValue:@84]; + + [self waitUntil:^BOOL{ return done; }]; +} + +- (void) testOnceChildAddedFiresOnceEvenWithCompoundObject { + FIRDatabaseReference * path = [FTestHelpers getRandomNode]; + __block BOOL firstCall = YES; + + [path observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue(firstCall, @"Properly saw first call"); + firstCall = NO; + XCTAssertEqualObjects(@84, [snapshot value], @"Properly saw node value"); + XCTAssertEqualObjects(@"bar", [snapshot key], @"Properly saw the first node"); + }]; + + [path setValue:@{@"foo": @42, @"bar": @84}]; + + __block BOOL done = NO; + + [path setValue:nil withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { done = YES; }]; + [self waitUntil:^BOOL{ return done; }]; +} + +- (void) testOnEmptyChildFires { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + __block BOOL done = NO; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + [[node child:@"test"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"Properly saw nil child node"); + done = YES; + }]; + + [self waitUntil:^BOOL{ return done; }]; +} + + +- (void) testOnEmptyChildEvenAfterParentIsSynched { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + __block BOOL parentDone = NO; + __block BOOL done = NO; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + parentDone = YES; + }]; + + [self waitUntil:^BOOL{ + return parentDone; + }]; + + [[node child:@"test"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([[snapshot value] isEqual:[NSNull null]], @"Child is properly nil"); + done = YES; + }]; + + // This test really isn't in the same spirit as the JS test; we can't currently make sure that the test fires right away since the ON and callback are async + + [self waitUntil:^BOOL{ + return done; + }]; + + XCTAssertTrue(done, @"Done fired."); +} + +- (void) testEventsAreRaisedChildRemovedChildAddedChildMoved { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSMutableArray* events = [[NSMutableArray alloc] init]; + + [node observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snap) { + [events addObject:[NSString stringWithFormat:@"added %@", [snap key]]]; + }]; + + [node observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snap) { + [events addObject:[NSString stringWithFormat:@"removed %@", [snap key]]]; + }]; + + [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snap) { + [events addObject:[NSString stringWithFormat:@"moved %@", [snap key]]]; + }]; + + __block BOOL done = NO; + + [node setValue:@{ + @"a": @{@".value": @1, @".priority": @0 }, + @"b": @{@".value": @1, @".priority": @1 }, + @"c": @{@".value": @1, @".priority": @2 }, + @"d": @{@".value": @1, @".priority": @3 }, + @"e": @{@".value": @1, @".priority": @4 }, + @"f": @{@".value": @1, @".priority": @5 }, + } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [events removeAllObjects]; + + done = NO; + + [node setValue:@{ + @"a": @{@".value": @1, @".priority": @5 }, + @"aa": @{@".value": @1, @".priority": @0 }, + @"b": @{@".value": @1, @".priority": @1 }, + @"bb": @{@".value": @1, @".priority": @2 }, + @"d": @{@".value": @1, @".priority": @3 }, + @"e": @{@".value": @1, @".priority": @6 }, + } + withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + } + ]; + + [self waitUntil:^BOOL{ + return done; + }]; + + XCTAssertEqualObjects(@"removed c, removed f, added aa, added bb, moved a, moved e", [events componentsJoinedByString:@", "], @"Got expected results"); +} + +- (void) testIntegerToDoubleConversions { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSMutableArray<NSString *>* events = [[NSMutableArray alloc] init]; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snap) { + [events addObject:[NSString stringWithFormat:@"value %@", [snap value]]]; + }]; + + for(NSNumber *number in @[@1, @1.0, @1, @1.1]) { + [self waitForCompletionOf:node setValue:number]; + } + + XCTAssertEqualObjects(@"value 1, value 1.1", [events componentsJoinedByString:@", "], + @"Got expected results"); + +} + +- (void) testEventsAreRaisedProperlyWithOnQueryLimits { + // xxx impl query +} + +@end diff --git a/Example/Database/Tests/Integration/FIRAuthTests.m b/Example/Database/Tests/Integration/FIRAuthTests.m new file mode 100644 index 0000000..2c44580 --- /dev/null +++ b/Example/Database/Tests/Integration/FIRAuthTests.m @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FIRApp.h" +#import "FTestHelpers.h" +#import "FTestAuthTokenGenerator.h" +#import "FIRTestAuthTokenProvider.h" +#import "FIRDatabaseConfig_Private.h" +#import "FTestBase.h" + +@interface FIRAuthTests : FTestBase + +@end + +@implementation FIRAuthTests + +- (void)setUp { + [super setUp]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testListensAndAuthRaceCondition { + [FIRDatabase setLoggingEnabled:YES]; + id<FAuthTokenProvider> tokenProvider = [FAuthTokenProvider authTokenProviderForApp:[FIRApp defaultApp]]; + + FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:@"testWritesRestoredAfterAuth"]; + config.authTokenProvider = tokenProvider; + + FIRDatabaseReference *ref = [[[FIRDatabaseReference alloc] initWithConfig:config] childByAutoId]; + + __block BOOL done = NO; + + [[[ref root] child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^void( + FIRDataSnapshot *snapshot) { + if ([snapshot.value boolValue]) { + // Start a listen before auth credentials are restored. + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + + }]; + + // subsequent writes should complete successfully. + [ref setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + } + }]; + + WAIT_FOR(done); +} +@end diff --git a/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h new file mode 100644 index 0000000..d6074ac --- /dev/null +++ b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import "FTestBase.h" + +@interface FIRDatabaseQueryTests : FTestBase + +@end diff --git a/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m new file mode 100644 index 0000000..a5bff5a --- /dev/null +++ b/Example/Database/Tests/Integration/FIRDatabaseQueryTests.m @@ -0,0 +1,2780 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRDatabaseQueryTests.h" +#import "FIRDatabaseQuery_Private.h" +#import "FQuerySpec.h" +#import "FTestExpectations.h" + +@implementation FIRDatabaseQueryTests + +- (void) testCanCreateBasicQueries { + // Just make sure none of these throw anything + + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + [ref queryLimitedToFirst:10]; + [ref queryLimitedToLast:10]; + + [[ref queryOrderedByKey] queryStartingAtValue:@"foo"]; + [[ref queryOrderedByKey] queryEndingAtValue:@"foo"]; + [[ref queryOrderedByKey] queryEqualToValue:@"foo"]; + + [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@YES]; + [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@1]; + [[ref queryOrderedByChild:@"index"] queryStartingAtValue:@"foo"]; + [[ref queryOrderedByChild:@"index"] queryStartingAtValue:nil]; + [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@YES]; + [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@1]; + [[ref queryOrderedByChild:@"index"] queryEndingAtValue:@"foo"]; + [[ref queryOrderedByChild:@"index"] queryEndingAtValue:nil]; + [[ref queryOrderedByChild:@"index"] queryEqualToValue:@YES]; + [[ref queryOrderedByChild:@"index"] queryEqualToValue:@1]; + [[ref queryOrderedByChild:@"index"] queryEqualToValue:@"foo"]; + [[ref queryOrderedByChild:@"index"] queryEqualToValue:nil]; + + [[ref queryOrderedByPriority] queryStartingAtValue:@1]; + [[ref queryOrderedByPriority] queryStartingAtValue:@"foo"]; + [[ref queryOrderedByPriority] queryStartingAtValue:nil]; + [[ref queryOrderedByPriority] queryEndingAtValue:@1]; + [[ref queryOrderedByPriority] queryEndingAtValue:@"foo"]; + [[ref queryOrderedByPriority] queryEndingAtValue:nil]; + [[ref queryOrderedByPriority] queryEqualToValue:@1]; + [[ref queryOrderedByPriority] queryEqualToValue:@"foo"]; + [[ref queryOrderedByPriority] queryEqualToValue:nil]; +} + +- (void) testInvalidQueryParams { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + XCTAssertThrows([[ref queryLimitedToFirst:100] queryLimitedToFirst:100]); + XCTAssertThrows([[ref queryLimitedToFirst:100] queryLimitedToLast:100]); + XCTAssertThrows([[ref queryLimitedToLast:100] queryLimitedToFirst:100]); + XCTAssertThrows([[ref queryLimitedToLast:100] queryLimitedToLast:100]); + XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByPriority]); + XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByKey]); + XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByChild:@"foo"]); + XCTAssertThrows([[ref queryOrderedByPriority] queryOrderedByValue]); + XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByPriority]); + XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByKey]); + XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByChild:@"foo"]); + XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByValue]); + XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByPriority]); + XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByKey]); + XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByChild:@"foo"]); + XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByValue]); + XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByPriority]); + XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByKey]); + XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByChild:@"foo"]); + XCTAssertThrows([[ref queryOrderedByValue] queryOrderedByValue]); + XCTAssertThrows([[ref queryStartingAtValue:@"foo"] queryStartingAtValue:@"foo"]); + XCTAssertThrows([[ref queryStartingAtValue:@"foo"] queryEqualToValue:@"foo"]); + XCTAssertThrows([[ref queryEndingAtValue:@"foo"] queryEndingAtValue:@"foo"]); + XCTAssertThrows([[ref queryEndingAtValue:@"foo"] queryEqualToValue:@"foo"]); + XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryStartingAtValue:@"foo"]); + XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryEndingAtValue:@"foo"]); + XCTAssertThrows([[ref queryEqualToValue:@"foo"] queryEqualToValue:@"foo"]); + XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@"foo" childKey:@"foo"]); + XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@"foo" childKey:@"foo"]); + XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:@"foo" childKey:@"foo"]); + XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@1 childKey:@"foo"]); + XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@YES]); + XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@1]); + XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@YES]); + XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:nil]); + XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:nil]); + XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:nil]); + XCTAssertThrows([[ref queryStartingAtValue:@"foo" childKey:@"foo"] queryOrderedByKey]); + XCTAssertThrows([[ref queryEndingAtValue:@"foo" childKey:@"foo"] queryOrderedByKey]); + XCTAssertThrows([[ref queryEqualToValue:@"foo" childKey:@"foo"] queryOrderedByKey]); + XCTAssertThrows([[ref queryStartingAtValue:@1] queryOrderedByKey]); + XCTAssertThrows([[ref queryStartingAtValue:@YES] queryOrderedByKey]); + XCTAssertThrows([[ref queryEndingAtValue:@1] queryOrderedByKey]); + XCTAssertThrows([[ref queryEndingAtValue:@YES] queryOrderedByKey]); + XCTAssertThrows([ref queryStartingAtValue:@[]]); + XCTAssertThrows([ref queryStartingAtValue:@{}]); + XCTAssertThrows([ref queryEndingAtValue:@[]]); + XCTAssertThrows([ref queryEndingAtValue:@{}]); + XCTAssertThrows([ref queryEqualToValue:@[]]); + XCTAssertThrows([ref queryEqualToValue:@{}]); + + XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByPriority], @"Cannot call orderBy multiple times"); + XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByPriority], @"Cannot call orderBy multiple times"); + XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByKey], @"Cannot call orderBy multiple times"); + XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByKey], @"Cannot call orderBy multiple times"); + XCTAssertThrows([[ref queryOrderedByKey] queryOrderedByChild:@"foo"], @"Cannot call orderBy multiple times"); + XCTAssertThrows([[ref queryOrderedByChild:@"foo"] queryOrderedByChild:@"foo"], @"Cannot call orderBy multiple times"); + + XCTAssertThrows([[ref queryOrderedByKey] queryStartingAtValue:@"a" childKey:@"b"], @"Cannot specify starting child name when ordering by key."); + XCTAssertThrows([[ref queryOrderedByKey] queryEndingAtValue:@"a" childKey:@"b"], @"Cannot specify ending child name when ordering by key."); + XCTAssertThrows([[ref queryOrderedByKey] queryEqualToValue:@"a" childKey:@"b"], @"Cannot specify equalTo child name when ordering by key."); + + XCTAssertThrows([[ref queryOrderedByPriority] queryStartingAtValue:@YES], @"Can't pass booleans as start/end when using priority index."); + XCTAssertThrows([[ref queryOrderedByPriority] queryEndingAtValue:@NO], @"Can't pass booleans as start/end when using priority index."); + XCTAssertThrows([[ref queryOrderedByPriority] queryEqualToValue:@YES], @"Can't pass booleans as start/end when using priority index."); +} + +- (void) testLimitRanges +{ + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + XCTAssertThrows([ref queryLimitedToLast:0], @"Can't pass zero as limit"); + XCTAssertThrows([ref queryLimitedToFirst:0], @"Can't pass zero as limit"); + XCTAssertThrows([ref queryLimitedToLast:0], @"Can't pass zero as limit"); + uint64_t MAX_ALLOWED_VALUE = (1l << 31) - 1; + [ref queryLimitedToFirst:MAX_ALLOWED_VALUE]; + [ref queryLimitedToLast:MAX_ALLOWED_VALUE]; + XCTAssertThrows([ref queryLimitedToFirst:(MAX_ALLOWED_VALUE+1)], @"Can't pass limits that don't fit into 32 bit signed integer range"); + XCTAssertThrows([ref queryLimitedToLast:(MAX_ALLOWED_VALUE+1)], @"Can't pass limits that don't fit into 32 bit signed integer range"); +} + +- (void) testInvalidKeys { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSArray* badKeys = @[ @".test", @"test.", @"fo$o", @"[what", @"ever]", @"ha#sh", @"/thing", @"th/ing", @"thing/"]; + + for (NSString* badKey in badKeys) { + XCTAssertThrows([[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:badKey], @"Setting bad key"); + XCTAssertThrows([[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:badKey], @"Setting bad key"); + } +} + +- (void) testOffCanBeCalledOnDefault { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL called = NO; + FIRDatabaseQuery * query = [ref queryLimitedToLast:5]; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (called) { + XCTFail(@"Should not be called twice"); + } else { + called = YES; + } + }]; + + [ref setValue:@{@"a": @5, @"b": @6}]; + + [self waitUntil:^BOOL{ + return called; + }]; + + called = NO; + + [ref removeAllObservers]; + + __block BOOL complete = NO; + [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + complete = YES; + }]; + + [self waitUntil:^BOOL{ + return complete; + }]; + + XCTAssertFalse(called, @"Should not have been called again"); +} + +- (void) testOffCanBeCalledOnHandle { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL called = NO; + FIRDatabaseQuery * query = [ref queryLimitedToLast:5]; + FIRDatabaseHandle handle = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (called) { + XCTFail(@"Should not be called twice"); + } else { + called = YES; + } + }]; + + [ref setValue:@{@"a": @5, @"b": @6}]; + + [self waitUntil:^BOOL{ + return called; + }]; + + called = NO; + + [ref removeObserverWithHandle:handle]; + + __block BOOL complete = NO; + [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + complete = YES; + }]; + + [self waitUntil:^BOOL{ + return complete; + }]; + + XCTAssertFalse(called, @"Should not have been called again"); +} + +- (void) testOffCanBeCalledOnSpecificQuery { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL called = NO; + FIRDatabaseQuery * query = [ref queryLimitedToLast:5]; + FIRDatabaseHandle handle = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (called) { + XCTFail(@"Should not be called twice"); + } else { + called = YES; + } + }]; + + [ref setValue:@{@"a": @5, @"b": @6}]; + + [self waitUntil:^BOOL{ + return called; + }]; + + called = NO; + + [query removeObserverWithHandle:handle]; + + __block BOOL complete = NO; + [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + complete = YES; + }]; + + [self waitUntil:^BOOL{ + return complete; + }]; + + XCTAssertFalse(called, @"Should not have been called again"); +} + +- (void) testOffCanBeCalledOnMultipleQueries { + FIRDatabaseQuery *query = [[FTestHelpers getRandomNode] queryLimitedToFirst:10]; + FIRDatabaseHandle handle1 = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + FIRDatabaseHandle handle2 = [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + [query removeObserverWithHandle:handle1]; + [query removeObserverWithHandle:handle2]; +} + +- (void) testOffCanBeCalledWithoutHandle { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL called1 = NO; + __block BOOL called2 = NO; + FIRDatabaseQuery * query = [ref queryLimitedToLast:5]; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + called1 = YES; + }]; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + called2 = YES; + }]; + + [ref setValue:@{@"a": @5, @"b": @6}]; + + [self waitUntil:^BOOL{ + return called1 && called2; + }]; + + called1 = NO; + called2 = NO; + + [ref removeAllObservers]; + + __block BOOL complete = NO; + [ref setValue:@{@"a": @6, @"b": @7} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + complete = YES; + }]; + + [self waitUntil:^BOOL{ + return complete; + }]; + + XCTAssertFalse(called1 || called2, @"Should not have called either callback"); +} + +- (void) testEnsureOnly5ItemsAreKept { + __block FIRDataSnapshot * snap = nil; + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + FIRDatabaseQuery * query = [ref queryLimitedToLast:5]; + __block int count = 0; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + count++; + }]; + + [ref setValue:nil]; + for (int i = 0; i < 10; ++i) { + [[ref childByAutoId] setValue:[NSNumber numberWithInt:i]]; + } + + [self waitUntil:^BOOL{ + // The initial set triggers the callback, so we need to wait for 11 events + return count == 11; + }]; + + count = 5; + for (FIRDataSnapshot * snapshot in snap.children) { + NSNumber* num = [snapshot value]; + NSNumber* current = [NSNumber numberWithInt:count]; + XCTAssertTrue([num isEqualToNumber:current], @"Expect children in order"); + count++; + } + + XCTAssertTrue(count == 10, @"Expected 5 children"); +} + +- (void) testOnlyLast5SentFromServer { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block int count = 0; + + [ref setValue:nil]; + + for (int i = 0; i < 10; ++i) { + [[ref childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + count++; + }]; + } + + [self waitUntil:^BOOL{ + return count == 10; + }]; + + FIRDatabaseQuery * query = [ref queryLimitedToLast:5]; + count = 5; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + NSNumber *num = [child value]; + NSNumber *current = [NSNumber numberWithInt:count]; + XCTAssertTrue([num isEqualToNumber:current], @"Expect children to be in order"); + count++; + } + }]; + + [self waitUntil:^BOOL{ + return count == 10; + }]; +} + +- (void) testVariousLimits { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + [expectations addQuery:[ref queryLimitedToLast:1] withExpectation:@{@"c": @3}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:1] withExpectation:@{@"c": @3}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:2] withExpectation:@{@"b": @2, @"c": @3}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:3] withExpectation:@{@"a": @1, @"b": @2, @"c": @3}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryEndingAtValue:nil] queryLimitedToLast:4] withExpectation:@{@"a": @1, @"b": @2, @"c": @3}]; + + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [expectations validate]; +} + +- (void) testSetLimitsWithStartAt { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1] withExpectation:@{@"a": @1}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"c"] queryLimitedToFirst:1] withExpectation:@{@"c": @3}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:1] withExpectation:@{@"b": @2}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:2] withExpectation:@{@"b": @2, @"c": @3}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"b"] queryLimitedToFirst:3] withExpectation:@{@"b": @2, @"c": @3}]; + + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [expectations validate]; +} + +- (void) testLimitsAndStartAtWithServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1] withExpectation:@{@"a": @1}]; + + /*params = [[FQueryParams alloc] init]; + params = [params setStartPriority:nil andName:@"c"]; + params = [params limitTo:1]; + [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"c": @3}]; + + params = [[FQueryParams alloc] init]; + params = [params setStartPriority:nil andName:@"b"]; + params = [params limitTo:1]; + [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2}]; + + params = [[FQueryParams alloc] init]; + params = [params setStartPriority:nil andName:@"b"]; + params = [params limitTo:2]; + [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2, @"c": @3}]; + + params = [[FQueryParams alloc] init]; + params = [params setStartPriority:nil andName:@"b"]; + params = [params limitTo:3]; + [expectations addQuery:[ref queryWithParams:params] withExpectation:@{@"b": @2, @"c": @3}];*/ + + [self waitUntil:^BOOL{ + return expectations.isReady; + }]; + [expectations validate]; + [ref removeAllObservers]; +} + +- (void) testChildEventsAreFiredWhenLimitIsHit { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + [added addObject:[snapshot key]]; + }]; + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"b", @"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have two items"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"b"]; + XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b"); + expected = @[@"d"]; + XCTAssertTrue([added isEqualToArray:expected], @"Expected to add d"); + [ref removeAllObservers]; +} + +- (void) testChildEventsAreFiredWhenLimitIsHitWithServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + FIRDatabaseQuery * query = [ref queryLimitedToLast:2]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + [self waitUntil:^BOOL{ + return [added count] == 2; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"b", @"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have two items"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"b"]; + XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b"); + expected = @[@"d"]; + XCTAssertTrue([added isEqualToArray:expected], @"Expected to add d"); + [ref removeAllObservers]; +} + +- (void) testChildEventsAreFiredWhenLimitIsHitWithStart { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"a", @"b"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have two items"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"aa"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"b"]; + XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b"); + expected = @[@"aa"]; + XCTAssertTrue([added isEqualToArray:expected], @"Expected to add aa"); + [ref removeAllObservers]; +} + +- (void) testChildEventsAreFiredWhenLimitIsHitWithStartAndServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2]; + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + [self waitUntil:^BOOL{ + return [added count] == 2; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"a", @"b"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have two items"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"aa"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"b"]; + XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b"); + expected = @[@"aa"]; + XCTAssertTrue([added isEqualToArray:expected], @"Expected to add aa"); + [ref removeAllObservers]; +} + +- (void) testStartAndLimitWithIncompleteWindow { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2]; + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready && [added count] >= 1; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have one item"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"b"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([removed count] == 0, @"Expected to remove nothing"); + expected = @[@"b"]; + XCTAssertTrue([added isEqualToArray:expected], @"Expected to add b"); + [ref removeAllObservers]; +} + +- (void) testStartAndLimitWithIncompleteWindowAndServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"a"] queryLimitedToFirst:2]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + [self waitUntil:^BOOL{ + return [added count] == 1; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have one item"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"b"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([removed count] == 0, @"Expected to remove nothing"); + expected = @[@"b"]; + XCTAssertTrue([added isEqualToArray:expected], @"Expected to add b"); + [ref removeAllObservers]; +} + +- (void) testChildEventsFiredWhenItemDeleted { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + FIRDatabaseQuery * query = [ref queryLimitedToLast:2]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready && [added count] >= 1; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"b", @"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have one item"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"b"]; + XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b"); + expected = @[@"a"]; + XCTAssertTrue([added isEqualToArray:expected], @"Expected to add a"); + [ref removeAllObservers]; +} + +-(void) testChildEventsAreFiredWhenItemDeletedAtServer { + FIRDatabaseReference * ref = [FTestHelpers getRandomNodeWithoutPersistence]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FIRDatabaseQuery * query = [ref queryLimitedToLast:2]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + [self waitUntil:^BOOL{ + return [added count] == 2; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"b", @"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have two items"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertEqualObjects(removed, (@[@"b"]), @"Expected to remove b"); + XCTAssertEqualObjects(added, (@[@"a"]), @"Expected to add a"); + [ref removeAllObservers]; +} + +- (void) testRemoveFiredWhenItemDeleted { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + FIRDatabaseQuery * query = [ref queryLimitedToLast:2]; + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready && [added count] >= 1; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"b", @"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have one item"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"b"]; + XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b"); + XCTAssertTrue([added count] == 0, @"Expected to add nothing"); + [ref removeAllObservers]; +} + +-(void) testRemoveFiredWhenItemDeletedAtServer { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@{@"b": @2, @"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FIRDatabaseQuery * query = [ref queryLimitedToLast:2]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + NSMutableArray* removed = [[NSMutableArray alloc] init]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + + [added addObject:[snapshot key]]; + }]; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + + [removed addObject:[snapshot key]]; + }]; + + [self waitUntil:^BOOL{ + return [added count] == 2; + }]; + + XCTAssertTrue([removed count] == 0, @"Nothing should be removed from our window"); + NSArray* expected = @[@"b", @"c"]; + XCTAssertTrue([added isEqualToArray:expected], @"Should have two items"); + + [added removeAllObjects]; + ready = NO; + [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"b"]; + XCTAssertTrue([removed isEqualToArray:expected], @"Expected to remove b"); + XCTAssertTrue([added count] == 0, @"Expected to add nothing"); + [ref removeAllObservers]; +} + +- (void) testStartAtPriorityAndEndAtPriorityWork { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"y"] withExpectation:@{@"b": @2, @"c": @3, @"d": @4}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"w"] withExpectation:@{@"d": @4}]; + + __block id nullSnap = @"dummy"; + [[[[ref queryOrderedByPriority] queryStartingAtValue:@"a"] queryEndingAtValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nullSnap = [snapshot value]; + }]; + + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @"z"}, + @"b": @{@".value": @2, @".priority": @"y"}, + @"c": @{@".value": @3, @".priority": @"x"}, + @"d": @{@".value": @4, @".priority": @"w"} + }]; + + WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]); + + [expectations validate]; +} + +- (void) testStartAtPriorityAndEndAtPriorityWorkWithServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @"z"}, + @"b": @{@".value": @2, @".priority": @"y"}, + @"c": @{@".value": @3, @".priority": @"x"}, + @"d": @{@".value": @4, @".priority": @"w"} + } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"y"] withExpectation:@{@"b": @2, @"c": @3, @"d": @4}]; + [expectations addQuery:[[[ref queryOrderedByPriority] queryStartingAtValue:@"w"] queryEndingAtValue:@"w"] withExpectation:@{@"d": @4}]; + + __block id nullSnap = @"dummy"; + [[[[ref queryOrderedByPriority] queryStartingAtValue:@"a"] queryEndingAtValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nullSnap = [snapshot value]; + }]; + + WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]); + + [expectations validate]; +} + +- (void) testStartAtAndEndAtPriorityAndNameWork { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"a"] queryEndingAtValue:@2 childKey:@"d"]; + [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"b"] queryEndingAtValue:@2 childKey:@"c"]; + [expectations addQuery:query withExpectation:@{@"b": @2, @"c": @3}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2]; + [expectations addQuery:query withExpectation:@{@"c": @3, @"d": @4}]; + + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @1}, + @"b": @{@".value": @2, @".priority": @1}, + @"c": @{@".value": @3, @".priority": @2}, + @"d": @{@".value": @4, @".priority": @2} + }]; + + WAIT_FOR(expectations.isReady); + + [expectations validate]; +} + +- (void) testStartAtAndEndAtPriorityAndNameWorkWithServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block BOOL ready = NO; + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @1}, + @"b": @{@".value": @2, @".priority": @1}, + @"c": @{@".value": @3, @".priority": @2}, + @"d": @{@".value": @4, @".priority": @2} + } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"a"] queryEndingAtValue:@2 childKey:@"d"]; + [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"b"] queryEndingAtValue:@2 childKey:@"c"]; + [expectations addQuery:query withExpectation:@{@"b": @2, @"c": @3}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2]; + [expectations addQuery:query withExpectation:@{@"c": @3, @"d": @4}]; + + WAIT_FOR(expectations.isReady); + + [expectations validate]; +} + +- (void) testStartAtAndEndAtPriorityAndNameWork2 { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2 childKey:@"b"]; + [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"d"] queryEndingAtValue:@2 childKey:@"a"]; + [expectations addQuery:query withExpectation:@{@"d": @4, @"a": @1}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"e"] queryEndingAtValue:@2]; + [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2}]; + + [ref setValue:@{ + @"c": @{@".value": @3, @".priority": @1}, + @"d": @{@".value": @4, @".priority": @1}, + @"a": @{@".value": @1, @".priority": @2}, + @"b": @{@".value": @2, @".priority": @2} + }]; + + WAIT_FOR(expectations.isReady); + + [expectations validate]; +} + +- (void) testStartAtAndEndAtPriorityAndNameWorkWithServerData2 { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block BOOL ready = NO; + [ref setValue:@{ + @"c": @{@".value": @3, @".priority": @1}, + @"d": @{@".value": @4, @".priority": @1}, + @"a": @{@".value": @1, @".priority": @2}, + @"b": @{@".value": @2, @".priority": @2} + } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"c"] queryEndingAtValue:@2 childKey:@"b"]; + [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"d"] queryEndingAtValue:@2 childKey:@"a"]; + [expectations addQuery:query withExpectation:@{@"d": @4, @"a": @1}]; + + query = [[[ref queryOrderedByPriority] queryStartingAtValue:@1 childKey:@"e"] queryEndingAtValue:@2]; + [expectations addQuery:query withExpectation:@{@"a": @1, @"b": @2}]; + + WAIT_FOR(expectations.isReady); + + [expectations validate]; +} + +- (void) testEqualToPriorityWorks { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + [expectations addQuery:[[ref queryOrderedByPriority] queryEqualToValue:@"w"] withExpectation:@{@"d": @4}]; + + __block id nullSnap = @"dummy"; + [[[ref queryOrderedByPriority] queryEqualToValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nullSnap = [snapshot value]; + }]; + + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @"z"}, + @"b": @{@".value": @2, @".priority": @"y"}, + @"c": @{@".value": @3, @".priority": @"x"}, + @"d": @{@".value": @4, @".priority": @"w"} + }]; + + WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]); + + [expectations validate]; +} + +- (void) testEqualToPriorityWorksWithServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @"z"}, + @"b": @{@".value": @2, @".priority": @"y"}, + @"c": @{@".value": @3, @".priority": @"x"}, + @"d": @{@".value": @4, @".priority": @"w"} + } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + [expectations addQuery:[[ref queryOrderedByPriority] queryEqualToValue:@"w"] withExpectation:@{@"d": @4}]; + + __block id nullSnap = @"dummy"; + [[[ref queryOrderedByPriority] queryEqualToValue:@"c"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nullSnap = [snapshot value]; + }]; + + WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]); + + [expectations validate]; +} + +- (void) testEqualToPriorityAndNameWorks { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + FIRDatabaseQuery * query = [[ref queryOrderedByPriority] queryEqualToValue:@1 childKey:@"a"]; + [expectations addQuery:query withExpectation:@{@"a": @1}]; + + __block id nullSnap = @"dummy"; + [[[ref queryOrderedByPriority] queryEqualToValue:@"1" childKey:@"z"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nullSnap = [snapshot value]; + }]; + + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @1}, + @"b": @{@".value": @2, @".priority": @1}, + @"c": @{@".value": @3, @".priority": @2}, + @"d": @{@".value": @4, @".priority": @2} + }]; + + WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]); + + [expectations validate]; +} + +- (void) testEqualToPriorityAndNameWorksWithServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block BOOL ready = NO; + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @1}, + @"b": @{@".value": @2, @".priority": @1}, + @"c": @{@".value": @3, @".priority": @2}, + @"d": @{@".value": @4, @".priority": @2} + } withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + FTestExpectations* expectations = [[FTestExpectations alloc] initFrom:self]; + + FIRDatabaseQuery * query = [[ref queryOrderedByPriority] queryEqualToValue:@1 childKey:@"a"]; + [expectations addQuery:query withExpectation:@{@"a": @1}]; + + __block id nullSnap = @"dummy"; + [[[ref queryOrderedByPriority] queryEqualToValue:@"1" childKey:@"z"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nullSnap = [snapshot value]; + }]; + + WAIT_FOR(expectations.isReady && [nullSnap isEqual:[NSNull null]]); + + [expectations validate]; +} + +- (void) testPrevNameWorks { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSMutableArray* added = [[NSMutableArray alloc] init]; + + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + [added addObject:snapshot.key]; + if (prevName) { + [added addObject:prevName]; + } else { + [added addObject:@"null"]; + } + + }]; + + [[ref child:@"a"] setValue:@1]; + [self waitUntil:^BOOL{ + NSArray* expected = @[@"a", @"null"]; + return [added isEqualToArray:expected]; + }]; + + [added removeAllObjects]; + + [[ref child:@"c"] setValue:@3]; + [self waitUntil:^BOOL{ + NSArray* expected = @[@"c", @"a"]; + return [added isEqualToArray:expected]; + }]; + + [added removeAllObjects]; + + [[ref child:@"b"] setValue:@2]; + [self waitUntil:^BOOL{ + NSArray* expected = @[@"b", @"null"]; + return [added isEqualToArray:expected]; + }]; + + [added removeAllObjects]; + + [[ref child:@"d"] setValue:@3]; + [self waitUntil:^BOOL{ + NSArray* expected = @[@"d", @"c"]; + return [added isEqualToArray:expected]; + }]; +} + +// Dropping some of the server data tests here, around prevName. They don't really test anything new, and mostly don't even test server data + +- (void) testPrevNameWorksWithMoves { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSMutableArray* moved = [[NSMutableArray alloc] init]; + + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + [moved addObject:snapshot.key]; + if (prevName) { + [moved addObject:prevName]; + } else { + [moved addObject:@"null"]; + } + }]; + + [ref setValue:@{ + @"a": @{@".value": @"a", @".priority": @10}, + @"b": @{@".value": @"b", @".priority": @20}, + @"c": @{@".value": @"c", @".priority": @30}, + @"d": @{@".value": @"d", @".priority": @40} + }]; + + __block BOOL ready = NO; + [[ref child:@"c"] setPriority:@50 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSArray* expected = @[@"c", @"d"]; + XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild"); + + [moved removeAllObjects]; + ready = NO; + [[ref child:@"c"] setPriority:@35 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[@"c", @"null"]; + XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild"); + + [moved removeAllObjects]; + ready = NO; + [[ref child:@"b"] setPriority:@33 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + expected = @[]; + XCTAssertTrue([moved isEqualToArray:expected], @"Expected changed node and prevChild to be empty"); +} + +- (void) testLocalEvents { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSMutableArray* events = [[NSMutableArray alloc] init]; + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + NSString *eventString = [NSString stringWithFormat:@"%@ added", [snapshot value]]; + [events addObject:eventString]; + }]; + + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + NSString *eventString = [NSString stringWithFormat:@"%@ removed", [snapshot value]]; + [events addObject:eventString]; + }]; + + __block BOOL ready = NO; + for (int i = 0; i < 5; ++i) { + [[ref childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + if (i == 4) { + ready = YES; + } + }]; + } + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSArray* expected = @[@"0 added", @"1 added", @"0 removed", @"2 added", @"1 removed", @"3 added", @"2 removed", @"4 added"]; + XCTAssertTrue([events isEqualToArray:expected], @"Expecting window to stay at two nodes"); +} + +- (void) testRemoteEvents { + FTupleFirebase* pair = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = pair.one; + FIRDatabaseReference * reader = pair.two; + + NSMutableArray* events = [[NSMutableArray alloc] init]; + + [[reader queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + NSString *eventString = [NSString stringWithFormat:@"%@ added", [snapshot value]]; + [events addObject:eventString]; + }]; + + [[reader queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + NSString *oldEventString = [NSString stringWithFormat:@"%@ added", [snapshot value]]; + [events removeObject:oldEventString]; + }]; + + for (int i = 0; i < 5; ++i) { + [[writer childByAutoId] setValue:[NSNumber numberWithInt:i]]; + } + + NSArray* expected = @[@"3 added", @"4 added"]; + [self waitUntil:^BOOL{ + return [events isEqualToArray:expected]; + }]; +} + +- (void) testLimitOnEmptyNodeFiresValue { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testFilteringToNullPriorities { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + // Note: cannot set nil in a dictionary, just leave out priority + [ref setValue:@{ + @"a": @0, + @"b": @1, + @"c": @{@".priority": @2, @".value": @2}, + @"d": @{@".priority": @3, @".value": @3}, + @"e": @{@".priority": @"hi", @".value": @4} + }]; + + __block BOOL ready = NO; + [[[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryEndingAtValue:nil] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *expected = @{@"a" : @0, @"b" : @1}; + NSDictionary *val = [snapshot value]; + XCTAssertTrue([val isEqualToDictionary:expected], @"Expected only null priority keys"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testNullPrioritiesIncludedInEndAt { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + // Note: cannot set nil in a dictionary, just leave out priority + [ref setValue:@{ + @"a": @0, + @"b": @1, + @"c": @{@".priority": @2, @".value": @2}, + @"d": @{@".priority": @3, @".value": @3}, + @"e": @{@".priority": @"hi", @".value": @4} + }]; + + __block BOOL ready = NO; + [[[ref queryOrderedByPriority] queryEndingAtValue:@2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *expected = @{@"a" : @0, @"b" : @1, @"c" : @2}; + NSDictionary *val = [snapshot value]; + XCTAssertTrue([val isEqualToDictionary:expected], @"Expected up to priority 2"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (NSSet *) dumpListensForRef:(FIRDatabaseReference *)ref { + NSMutableSet* dumpPieces = [[NSMutableSet alloc] init]; + NSDictionary* listens = [ref.repo dumpListens]; + + FPath* nodePath = ref.path; + [listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *spec, id obj, BOOL *stop) { + if ([nodePath contains:spec.path]) { + FPath *relative = [FPath relativePathFrom:nodePath to:spec.path]; + [dumpPieces addObject:[[FQuerySpec alloc] initWithPath:relative params:spec.params]]; + } + }]; + + return dumpPieces; +} + +- (NSSet *) expectDefaultListenerAtPath:(FPath *)path { + return [self expectParams:[FQueryParams defaultInstance] atPath:path]; +} + +- (NSSet *) expectParamssetValue:(NSSet *)paramsSet atPath:(FPath *)path { + NSMutableSet *all = [NSMutableSet set]; + [paramsSet enumerateObjectsUsingBlock:^(FQueryParams *params, BOOL *stop) { + [all addObject:[[FQuerySpec alloc] initWithPath:path params:params]]; + }]; + return all; +} + +- (NSSet *) expectParams:(FQueryParams *)params atPath:(FPath *)path { + return [self expectParamssetValue:[NSSet setWithObject:params] atPath:path]; +} + +- (void) testDedupesListensOnChild { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block NSSet* listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No Listens yet"); + + [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + __block BOOL ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"a")]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected child listener"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"")]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [ref removeAllObservers]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [NSSet setWithObject:[FQuerySpec defaultQueryAtPath:PATH(@"a")]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Child listener should be back"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [[ref child:@"a"] removeAllObservers]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No more listeners"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testDedupeListensOnGrandchild { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block NSSet* listens; + __block BOOL ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No Listens yet"); + ready = YES; + }); + WAIT_FOR(ready); + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected one listener"); + ready = YES; + }); + WAIT_FOR(ready); + + [[ref child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener to override"); + ready = YES; + }); + WAIT_FOR(ready); + + [ref removeAllObservers]; + [[ref child:@"a/aa"] removeAllObservers]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No more listeners"); + ready = YES; + }); + WAIT_FOR(ready); +} + +- (void) testListenOnGrandparentOfTwoChildren { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block NSSet* listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No Listens yet"); + + [[ref child:@"a/aa"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + __block BOOL ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected grandchild"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [[ref child:@"a/bb"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expecteda = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]]; + NSSet* expectedb = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]]; + NSMutableSet* expected = [NSMutableSet setWithSet:expecteda]; + [expected unionSet:expectedb]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected two grandchildren"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected parent listener to override"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [ref removeAllObservers]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expecteda = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/aa"]]; + NSSet* expectedb = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]]; + NSMutableSet* expected = [NSMutableSet setWithSet:expecteda]; + [expected unionSet:expectedb]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected grandchild listeners to return"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [[ref child:@"a/aa"] removeAllObservers]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [self expectDefaultListenerAtPath:[FPath pathWithString:@"/a/bb"]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Expected one listener"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [[ref child:@"a/bb"] removeAllObservers]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No more listeners"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testDedupingMultipleListenQueries { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block NSSet* listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No Listens yet"); + + __block BOOL ready = NO; + FIRDatabaseQuery * aLim1 = [[ref child:@"a"] queryLimitedToLast:1]; + FIRDatabaseHandle handle1 = [aLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + FQueryParams* expectedParams = [[FQueryParams alloc] init]; + expectedParams = [expectedParams limitTo:1]; + NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Single query"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + FIRDatabaseQuery * rootLim1 = [ref queryLimitedToLast:1]; + FIRDatabaseHandle handle2 = [rootLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + FQueryParams* expectedParams = [[FQueryParams alloc] init]; + expectedParams = [expectedParams limitTo:1]; + NSSet* rootExpected = [self expectParams:expectedParams atPath:[FPath empty]]; + NSSet* childExpected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]]; + NSMutableSet* expected = [NSMutableSet setWithSet:rootExpected]; + [expected unionSet:childExpected]; + XCTAssertTrue([expected isEqualToSet:listens], @"Two queries"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + FIRDatabaseQuery * aLim5 = [[ref child:@"a"] queryLimitedToLast:5]; + FIRDatabaseHandle handle3 = [aLim5 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + FQueryParams* expectedParams1 = [[FQueryParams alloc] init]; + expectedParams1 = [expectedParams1 limitTo:1]; + NSSet* rootExpected = [self expectParams:expectedParams1 atPath:[FPath empty]]; + + FQueryParams* expectedParams2 = [[FQueryParams alloc] init]; + expectedParams2 = [expectedParams2 limitTo:5]; + NSSet* childExpected = [self expectParamssetValue:[NSSet setWithObjects:expectedParams1, expectedParams2, nil] atPath:[FPath pathWithString:@"/a"]]; + NSMutableSet* expected = [NSMutableSet setWithSet:childExpected]; + [expected unionSet:rootExpected]; + XCTAssertTrue([expected isEqualToSet:listens], @"Three queries"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [ref removeObserverWithHandle:handle2]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + FQueryParams* expectedParams1 = [[FQueryParams alloc] init]; + expectedParams1 = [expectedParams1 limitTo:1]; + FQueryParams* expectedParams2 = [[FQueryParams alloc] init]; + expectedParams2= [expectedParams2 limitTo:5]; + NSSet* expected = [self expectParamssetValue:[NSSet setWithObjects:expectedParams1, expectedParams2, nil] atPath:[FPath pathWithString:@"/a"]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Two queries"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [aLim1 removeObserverWithHandle:handle1]; + [aLim5 removeObserverWithHandle:handle3]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No more listeners"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testListenOnParentOfQueriedChildren { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block NSSet* listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No Listens yet"); + + __block BOOL ready = NO; + FIRDatabaseQuery * aLim1 = [[ref child:@"a"] queryLimitedToLast:1]; + FIRDatabaseHandle handle1 = [aLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + FQueryParams* expectedParams = [[FQueryParams alloc] init]; + expectedParams = [expectedParams limitTo:1]; + NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Single query"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + FIRDatabaseQuery * bLim1 = [[ref child:@"b"] queryLimitedToLast:1]; + FIRDatabaseHandle handle2 = [bLim1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + FQueryParams* expectedParams = [[FQueryParams alloc] init]; + expectedParams = [expectedParams limitTo:1]; + NSSet* expecteda = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/a"]]; + NSSet* expectedb = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/b"]]; + NSMutableSet* expected = [NSMutableSet setWithSet:expecteda]; + [expected unionSet:expectedb]; + XCTAssertTrue([expected isEqualToSet:listens], @"Two queries"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + FIRDatabaseHandle handle3 = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Parent should override"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + // remove in slightly random order + [aLim1 removeObserverWithHandle:handle1]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + NSSet* expected = [self expectDefaultListenerAtPath:[FPath empty]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Parent should override"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + [ref removeObserverWithHandle:handle3]; + ready = NO; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + FQueryParams* expectedParams = [[FQueryParams alloc] init]; + expectedParams = [expectedParams limitTo:1]; + NSSet* expected = [self expectParams:expectedParams atPath:[FPath pathWithString:@"/b"]]; + XCTAssertTrue([expected isEqualToSet:listens], @"Single query"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [bLim1 removeObserverWithHandle:handle2]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + listens = [self dumpListensForRef:ref]; + XCTAssertTrue(listens.count == 0, @"No more listeners"); + ready = YES; + }); + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +-(void) testLimitWithMixOfNullAndNonNullPriorities { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSMutableArray* children = [[NSMutableArray alloc] init]; + + [[ref queryLimitedToLast:5] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [children addObject:[snapshot key]]; + }]; + + __block BOOL ready = NO; + NSDictionary* toSet = @{ + @"Vikrum": @{@".priority": @1000, @"score": @1000, @"name": @"Vikrum"}, + @"Mike": @{@".priority": @500, @"score": @500, @"name": @"Mike"}, + @"Andrew": @{@".priority": @50, @"score": @50, @"name": @"Andrew"}, + @"James": @{@".priority": @7, @"score": @7, @"name": @"James"}, + @"Sally": @{@".priority": @-7, @"score": @-7, @"name": @"Sally"}, + @"Fred": @{@"score": @0, @"name": @"Fred"} + }; + + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSArray* expected = @[@"Sally", @"James", @"Andrew", @"Mike", @"Vikrum"]; + XCTAssertTrue([children isEqualToArray:expected], @"Null priority should be left out"); + +} + +-(void) testLimitWithMixOfNullAndNonNullPrioritiesOnServerData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + NSDictionary* toSet = @{ + @"Vikrum": @{@".priority": @1000, @"score": @1000, @"name": @"Vikrum"}, + @"Mike": @{@".priority": @500, @"score": @500, @"name": @"Mike"}, + @"Andrew": @{@".priority": @50, @"score": @50, @"name": @"Andrew"}, + @"James": @{@".priority": @7, @"score": @7, @"name": @"James"}, + @"Sally": @{@".priority": @-7, @"score": @-7, @"name": @"Sally"}, + @"Fred": @{@"score": @0, @"name": @"Fred"} + }; + + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + __block int count = 0; + NSMutableArray* children = [[NSMutableArray alloc] init]; + + [[ref queryLimitedToLast:5] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [children addObject:[snapshot key]]; + count++; + }]; + + [self waitUntil:^BOOL{ + return count == 5; + }]; + + + NSArray* expected = @[@"Sally", @"James", @"Andrew", @"Mike", @"Vikrum"]; + XCTAssertTrue([children isEqualToArray:expected], @"Null priority should be left out"); + +} + +// Skipping context tests. Context is not implemented on iOS + +/* DISABLING for now, since I'm not 100% sure what the right behavior is. + Perhaps a merge at /foo should shadow server updates at /foo instead of + just the modified children? Not sure. +- (void) testHandleUpdateThatDeletesEntireWindow { + Firebase* ref = [FTestHelpers getRandomNode]; + + NSMutableArray* snaps = [[NSMutableArray alloc] init]; + + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + if (val == nil) { + [snaps addObject:[NSNull null]]; + } else { + [snaps addObject:val]; + } + }]; + + NSDictionary* toSet = @{ + @"a": @{@".priority": @1, @".value": @1}, + @"b": @{@".priority": @2, @".value": @2}, + @"c": @{@".priority": @3, @".value": @3} + }; + + [ref setValue:toSet]; + + __block BOOL ready = NO; + toSet = @{@"b": [NSNull null], @"c": [NSNull null]}; + [ref updateChildValues:toSet withCompletionBlock:^(NSError* err, Firebase* ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSArray* expected = @[@{@"b": @2, @"c": @3}, [NSNull null], @{@"a": @1}]; + STAssertTrue([snaps isEqualToArray:expected], @"Expected %@ to equal %@", snaps, expected); +} +*/ + +- (void) testHandlesAnOutOfViewQueryOnAChild { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block NSDictionary* parent = nil; + [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + parent = [snapshot value]; + }]; + + __block NSNumber* child = nil; + [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + child = [snapshot value]; + }]; + + __block BOOL ready = NO; + NSDictionary* toSet = @{@"a": @1, @"b": @2}; + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSDictionary* parentExpected = @{@"b": @2}; + NSNumber* childExpected = [NSNumber numberWithInt:1]; + XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element"); + XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a"); + + ready = NO; + [ref updateChildValues:@{@"c": @3} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + parentExpected = @{@"c": @3}; + XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element"); + XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a"); +} + +- (void) testHandlesAChildQueryGoingOutOfViewOfTheParent { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block NSDictionary* parent = nil; + [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + parent = [snapshot value]; + }]; + + __block NSNumber* child = nil; + [[ref child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + child = [snapshot value]; + }]; + + __block BOOL ready = NO; + NSDictionary* toSet = @{@"a": @1}; + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + NSDictionary* parentExpected = @{@"a": @1}; + NSNumber* childExpected = [NSNumber numberWithInt:1]; + XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element"); + XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a"); + + ready = NO; + [[ref child:@"b"] setValue:@2 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + parentExpected = @{@"b": @2}; + XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element"); + XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a"); + + ready = NO; + [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + parentExpected = @{@"a": @1}; + XCTAssertTrue([parent isEqualToDictionary:parentExpected], @"Expected last element"); + XCTAssertTrue([child isEqualToNumber:childExpected], @"Expected value of a"); +} + +- (void) testHandlesDivergingViews { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block NSDictionary* cVal = nil; + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"c"] queryLimitedToLast:1]; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + cVal = [snapshot value]; + }]; + + __block NSDictionary* dVal = nil; + query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"d"] queryLimitedToLast:1]; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + dVal = [snapshot value]; + }]; + + __block BOOL ready = NO; + NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3}; + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSDictionary* expected = @{@"c": @3}; + XCTAssertTrue([cVal isEqualToDictionary:expected], @"should be c"); + XCTAssertTrue([dVal isEqualToDictionary:expected], @"should be c"); + + ready = NO; + [[ref child:@"d"] setValue:@4 withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([cVal isEqualToDictionary:expected], @"should be c"); + expected = @{@"d": @4}; + XCTAssertTrue([dVal isEqualToDictionary:expected], @"should be d"); +} + +- (void) testHandlesRemovingAQueriedElement { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block NSNumber* val = nil; + [[ref queryLimitedToLast:1] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + id newVal = [snapshot value]; + if (newVal != nil) { + val = [snapshot value]; + } + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([val isEqualToNumber:@2], @"Expected last element in window"); + + ready = NO; + [[ref child:@"b"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([val isEqualToNumber:@1], @"Should now be the next element in the window"); +} + +- (void) testStartAtAndLimit1Works { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block NSNumber* val = nil; + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + id newVal = [snapshot value]; + if (newVal != nil) { + val = [snapshot value]; + } + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([val isEqualToNumber:@1], @"Expected first element in window"); +} + +// See case 1664 +- (void) testStartAtAndLimit1AndRemoveFirstChild { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block NSNumber* val = nil; + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:nil] queryLimitedToFirst:1]; + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + id newVal = [snapshot value]; + if (newVal != nil) { + val = [snapshot value]; + } + }]; + + __block BOOL ready = NO; + [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([val isEqualToNumber:@1], @"Expected first element in window"); + + ready = NO; + [[ref child:@"a"] removeValueWithCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([val isEqualToNumber:@2], @"Expected next element in window"); +} + +// See case 1169 +- (void) testStartAtWithTwoArgumentsWorks { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + NSMutableArray* children = [[NSMutableArray alloc] init]; + + NSDictionary* toSet = @{ + @"Walker": @{@"name": @"Walker", @"score": @20, @".priority": @20}, + @"Michael": @{@"name": @"Michael", @"score": @100, @".priority": @100} + }; + + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryStartingAtValue:@20 childKey:@"Walker"] queryLimitedToFirst:2]; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + + for (FIRDataSnapshot *child in snapshot.children) { + [children addObject:child.key]; + } + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSArray* expected = @[@"Walker", @"Michael"]; + XCTAssertTrue([children isEqualToArray:expected], @"Expected both children"); +} + +- (void) testHandlesMultipleQueriesOnSameNode { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + + NSDictionary* toSet = @{ + @"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5, @"f": @6 + }; + + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + __block BOOL called = NO; + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // we got the initial data + XCTAssertFalse(called, @"This should only get called once, we don't update data after this"); + called = YES; + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + __block NSDictionary* snap = nil; + // now do nested once calls + [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + + snap = [snapshot value]; + ready = YES; + }]; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSDictionary* expected = @{@"f": @6}; + XCTAssertTrue([snap isEqualToDictionary:expected], @"Expected the correct data"); +} + +- (void) testHandlesOnceCalledOnNodeWithDefaultListener { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + + NSDictionary* toSet = @{ + @"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5, @"f": @6 + }; + + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // we got the initial data + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + + __block NSNumber* snap = nil; + [[ref queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + snap = [snapshot value]; + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue([snap isEqualToNumber:@6], @"Got once response"); +} + +- (void) testHandlesOnceCalledOnNodeWithDefaultListenerAndNonCompleteLimit { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + + NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3}; + + [ref setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + // do first listen + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + + __block NSDictionary* snap = nil; + [[ref queryLimitedToLast:5] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = [snapshot value]; + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + NSDictionary* expected = @{@"a": @1, @"b": @2, @"c": @3}; + XCTAssertTrue([snap isEqualToDictionary:expected], @"Got once response"); +} + +- (void) testRemoveTriggersRemoteEvents { + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = tuple.one; + FIRDatabaseReference * reader = tuple.two; + + __block BOOL ready = NO; + + NSDictionary* toSet = @{@"a": @"a", @"b": @"b", @"c": @"c", @"d": @"d", @"e": @"e"}; + + [writer setValue:toSet withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + __block int count = 0; + + [[reader queryLimitedToLast:5] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + count++; + if (count == 1) { + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"a" : @"a", @"b" : @"b", @"c" : @"c", @"d" : @"d", @"e" : @"e"}; + XCTAssertTrue([val isEqualToDictionary:expected], @"First callback, expect all the data"); + [[writer child:@"c"] removeValue]; + } else { + XCTAssertTrue(count == 2, @"Should only get called twice"); + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"a" : @"a", @"b" : @"b", @"d" : @"d", @"e" : @"e"}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Second callback, expect all the remaining data"); + ready = YES; + } + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testEndingAtNameReturnsCorrectChildren { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSDictionary* toSet = @{ + @"a": @"a", + @"b": @"b", + @"c": @"c", + @"d": @"d", + @"e": @"e", + @"f": @"f", + @"g": @"g", + @"h": @"h" + }; + + [self waitForCompletionOf:ref setValue:toSet]; + + __block NSDictionary* snap = nil; + __block BOOL done = NO; + FIRDatabaseQuery * query = [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"f"] queryLimitedToLast:5]; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = [snapshot value]; + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + NSDictionary* expected = @{ + @"b": @"b", + @"c": @"c", + @"d": @"d", + @"e": @"e", + @"f": @"f" + }; + XCTAssertTrue([snap isEqualToDictionary:expected], @"Expected 5 elements, ending at f"); +} + +- (void) testListenForChildAddedWithLimitEnsureEventsFireProperly { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL done = NO; + + NSDictionary* toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}}; + [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + WAIT_FOR(done); + + __block int count = 0; + [[reader queryLimitedToLast:3] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + count++; + if (count == 1) { + XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child"); + XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value"); + } else if (count == 2) { + XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child"); + XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value"); + } else if (count == 3) { + XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child"); + NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}}; + XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object"); + } else { + XCTFail(@"wrong event count"); + } + }]; + + WAIT_FOR(count == 3); +} + + +- (void) testListenForChildChangedWithLimitEnsureEventsFireProperly { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL done = NO; + + NSDictionary* toSet = @{@"a": @"something", @"b": @"we'll", @"c": @"overwrite"}; + [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + WAIT_FOR(done); + + __block int count = 0; + [reader observeEventType:FIRDataEventTypeChildChanged withBlock:^(FIRDataSnapshot *snapshot) { + count++; + if (count == 1) { + XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child"); + XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value"); + } else if (count == 2) { + XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child"); + XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value"); + } else if (count == 3) { + XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child"); + NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}}; + XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object"); + } else { + XCTFail(@"wrong event count"); + } + }]; + toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}}; + [writer setValue:toSet]; + + WAIT_FOR(count == 3); +} + +- (void) testListenForChildRemovedWithLimitEnsureEventsFireProperly { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL done = NO; + + NSDictionary* toSet = @{@"a": @1, @"b": @"b", @"c": @{@"deep": @"path", @"of": @{@"stuff": @YES}}}; + [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + WAIT_FOR(done); + + __block int count = 0; + [reader observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + count++; + if (count == 1) { + XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Got first child"); + XCTAssertTrue([snapshot.value isEqualToNumber:@1], @"Got correct value"); + } else if (count == 2) { + XCTAssertTrue([snapshot.key isEqualToString:@"b"], @"Got second child"); + XCTAssertTrue([snapshot.value isEqualToString:@"b"], @"got correct value"); + } else if (count == 3) { + XCTAssertTrue([snapshot.key isEqualToString:@"c"], @"Got third child"); + NSDictionary *expected = @{@"deep" : @"path", @"of" : @{@"stuff" : @YES}}; + XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got deep object"); + } else { + XCTFail(@"wrong event count"); + } + }]; + + done = NO; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // Load the data first + done = snapshot.value != [NSNull null] && [snapshot.value isEqualToDictionary:toSet]; + }]; + + WAIT_FOR(done); + + // Now do the removes + [[writer child:@"a"] removeValue]; + [[writer child:@"b"] removeValue]; + [[writer child:@"c"] removeValue]; + + WAIT_FOR(count == 3); +} + +- (void) testQueriesBehaveProperlyAfterOnceCall { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + + __block BOOL done = NO; + NSDictionary* toSet = @{@"a": @1, @"b": @2, @"c": @3, @"d": @4}; + [writer setValue:toSet withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + WAIT_FOR(done); + + done = NO; + [reader observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = YES; + }]; + + WAIT_FOR(done); + + // Ok, now do some queries + __block int startCount = 0; + __block int defaultCount = 0; + [[[reader queryOrderedByPriority] queryStartingAtValue:nil childKey:@"d"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + startCount++; + }]; + + [reader observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + defaultCount++; + }]; + + [reader observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + XCTFail(@"Should not remove any children"); + }]; + + WAIT_FOR(startCount == 1 && defaultCount == 4); +} + +- (void) testIntegerKeysBehaveNumerically1 { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES }; + __block BOOL done = NO; + [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + [[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"80"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *expected = @{@"80" : @YES, @"550" : @YES, @"600" : @YES}; + XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result."); + done = YES; + }]; + }]; + WAIT_FOR(done); +} + +- (void) testIntegerKeysBehaveNumerically2 { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES }; + __block BOOL done = NO; + [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + [[[ref queryOrderedByPriority] queryEndingAtValue:nil childKey:@"50"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *expected = @{@"1" : @YES, @"6" : @YES, @"8" : @YES, @"50" : @YES}; + XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result."); + done = YES; + }]; + }]; + WAIT_FOR(done); +} + +- (void) testIntegerKeysBehaveNumerically3 { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSDictionary* toSet = @{@"1": @YES, @"50": @YES, @"550": @YES, @"6": @YES, @"600": @YES, @"70": @YES, @"8": @YES, @"80": @YES }; + __block BOOL done = NO; + [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + [[[[ref queryOrderedByPriority] queryStartingAtValue:nil childKey:@"50"] queryEndingAtValue:nil childKey:@"80"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *expected = @{@"50" : @YES, @"70" : @YES, @"80" : @YES}; + XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Got correct result."); + done = YES; + }]; + }]; + WAIT_FOR(done); +} + +- (void) testItemsPulledIntoLimitCorrectly { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSMutableArray* snaps = [[NSMutableArray alloc] init]; + + // Just so everything is cached locally. + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + + }]; + + [[ref queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + [snaps addObject:val]; + }]; + + [ref setValue:@{ + @"a": @{@".value": @1, @".priority": @1}, + @"b": @{@".value": @2, @".priority": @2}, + @"c": @{@".value": @3, @".priority": @3} + }]; + + __block BOOL ready = NO; + [[ref child:@"b"] setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + ready = YES; + }]; + + WAIT_FOR(ready); + + NSArray* expected = @[@{@"b": @2, @"c": @3}, @{@"a": @1, @"c": @3}]; + XCTAssertEqualObjects(snaps, expected, @"Incorrect snapshots."); +} + +- (void)testChildChangedCausesChildRemovedEvent +{ + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + [[ref child:@"l/a"] setValue:@"1" andPriority:@"a"]; + [[ref child:@"l/b"] setValue:@"2" andPriority:@"b"]; + FIRDatabaseQuery *query = [[[[ref child:@"l"] queryOrderedByPriority] queryStartingAtValue:@"b"] queryEndingAtValue:@"d"]; + __block BOOL removed = NO; + [query observeEventType:FIRDataEventTypeChildRemoved withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqualObjects(snapshot.value, @"2", @"Incorrect snapshot"); + removed = YES; + }]; + + [[ref child:@"l/b"] setValue:@"4" andPriority:@"a"]; + + WAIT_FOR(removed); +} + +- (void) testQueryHasRef { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + FIRDatabaseQuery *query = [ref queryOrderedByKey]; + XCTAssertEqualObjects([query.ref path], [ref path], @"Should have same path"); +} + +- (void) testQuerySnapshotChildrenRespectDefaultOrdering { + FTupleFirebase* pair = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = pair.one; + FIRDatabaseReference * reader = pair.two; + __block BOOL done = NO; + + NSDictionary* list = @{ + @"a": @{ + @"thisvaluefirst": @{ @".value": @true, @".priority": @1 }, + @"name": @{ @".value": @"Michael", @".priority": @2 }, + @"thisvaluelast": @{ @".value": @true, @".priority": @3 }, + }, + @"b": @{ + @"thisvaluefirst": @{ @".value": @true }, + @"name": @{ @".value": @"Rob", @".priority": @2 }, + @"thisvaluelast": @{ @".value": @true, @".priority": @3 }, + }, + @"c": @{ + @"thisvaluefirst": @{ @".value": @true, @".priority": @1 }, + @"name": @{ @".value": @"Jonny", @".priority": @2 }, + @"thisvaluelast": @{ @".value": @true, @".priority": @"somestring" }, + } + }; + + [writer setValue:list withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + WAIT_FOR(done); + + done = NO; + [[reader queryOrderedByChild:@"name"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSArray *expectedKeys = @[@"thisvaluefirst", @"name", @"thisvaluelast"]; + NSArray *expectedNames = @[@"Jonny", @"Michael", @"Rob"]; + + // Validate that snap.child() resets order to default for child snaps + NSMutableArray *orderedKeys = [[NSMutableArray alloc] init]; + for (FIRDataSnapshot *childSnap in [snapshot childSnapshotForPath:@"b"].children) { + [orderedKeys addObject:childSnap.key]; + } + XCTAssertEqualObjects(expectedKeys, orderedKeys, @"Should have matching ordered lists of keys"); + + // Validate that snap.forEach() resets ordering to default for child snaps + NSMutableArray *orderedNames = [[NSMutableArray alloc] init]; + for (FIRDataSnapshot *childSnap in snapshot.children) { + [orderedNames addObject:[childSnap childSnapshotForPath:@"name"].value]; + [orderedKeys removeAllObjects]; + for (FIRDataSnapshot *grandchildSnap in childSnap.children) { + [orderedKeys addObject:grandchildSnap.key]; + } + XCTAssertEqualObjects(expectedKeys, orderedKeys, @"Should have matching ordered lists of keys"); + } + XCTAssertEqualObjects(expectedNames, orderedNames, @"Should have matching ordered lists of names"); + + done = YES; + }]; + WAIT_FOR(done); +} + +- (void) testAddingListensForTheSamePathDoesNotCheckFail { + // This bug manifests itself if there's a hierarchy of query listener, default listener and one-time listener + // underneath. + // In Java implementation, during one-time listener registration, sync-tree traversal stopped as soon as it found + // a complete server cache (this is the case for not indexed query view). The problem is that the same traversal was + // looking for a ancestor default view, and the early exit prevented from finding the default listener above the + // one-time listener. Event removal code path wasn't removing the listener because it stopped as soon as it + // found the default view. This left the zombie one-time listener and check failed on the second attempt to + // create a listener for the same path (asana#61028598952586). + + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + + [[ref child:@"child"] setValue:@{@"name": @"John"}]; + [[[ref queryOrderedByChild:@"name"] queryEqualToValue:@"John"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = YES; + }]; + WAIT_FOR(done); + + done = NO; + [[[ref child:@"child"] child:@"favoriteToy"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = YES; + }]; + WAIT_FOR(done); + + done = NO; + [[[ref child:@"child"] child:@"favoriteToy"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = YES; + }]; + WAIT_FOR(done); +} + +@end diff --git a/Example/Database/Tests/Integration/FIRDatabaseTests.m b/Example/Database/Tests/Integration/FIRDatabaseTests.m new file mode 100644 index 0000000..8a5742d --- /dev/null +++ b/Example/Database/Tests/Integration/FIRDatabaseTests.m @@ -0,0 +1,375 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FIRApp.h" +#import "FIRDatabaseReference.h" +#import "FIRDatabaseReference_Private.h" +#import "FIRDatabase.h" +#import "FIRDatabaseConfig_Private.h" +#import "FIROptions.h" +#import "FTestHelpers.h" +#import "FMockStorageEngine.h" +#import "FTestBase.h" +#import "FTestHelpers.h" +#import "FIRFakeApp.h" + +@interface FIRDatabaseTests : FTestBase + +@end + +static const NSInteger kFErrorCodeWriteCanceled = 3; + +@implementation FIRDatabaseTests + +- (void) testFIRDatabaseForNilApp { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrowsSpecificNamed([FIRDatabase databaseForApp:nil], NSException, @"InvalidFIRApp"); +#pragma clang diagnostic pop +} + +- (void) testDatabaseForApp { + FIRDatabase *database = [self databaseForURL:self.databaseURL]; + XCTAssertEqualObjects(self.databaseURL, [database reference].URL); +} + +- (void) testDatabaseForAppWithInvalidURLs { + XCTAssertThrows([self databaseForURL:nil]); + XCTAssertThrows([self databaseForURL:@"not-a-url"]); + XCTAssertThrows([self databaseForURL:@"http://x.example.com/paths/are/bad"]); +} + +- (void) testReferenceWithPath { + FIRDatabase *db = [self defaultDatabase]; + NSString *expectedURL = [NSString stringWithFormat:@"%@/foo", self.databaseURL]; + XCTAssertEqualObjects(expectedURL, [db referenceWithPath:@"foo"].URL); +} + +- (void) testReferenceFromURLWithEmptyPath { + FIRDatabaseReference *ref = [[self defaultDatabase] referenceFromURL:self.databaseURL]; + XCTAssertEqualObjects(self.databaseURL, ref.URL); +} + +- (void) testReferenceFromURLWithPath { + NSString *url = [NSString stringWithFormat:@"%@/foo/bar", self.databaseURL]; + FIRDatabaseReference *ref = [[self defaultDatabase] referenceFromURL:url]; + XCTAssertEqualObjects(url, ref.URL); +} + +- (void) testReferenceFromURLWithWrongURL { + NSString *url = [NSString stringWithFormat:@"%@/foo/bar", @"https://foobar.firebaseio.com"]; + XCTAssertThrows([[self defaultDatabase] referenceFromURL:url]); +} + +- (void) testReferenceEqualityForFIRDatabase { + FIRDatabase *db1 = [self databaseForURL:self.databaseURL name:@"db1"]; + FIRDatabase *db2 = [self databaseForURL:self.databaseURL name:@"db2"]; + FIRDatabase *altDb = [self databaseForURL:self.databaseURL name:@"altDb"]; + FIRDatabase *wrongHostDb = [self databaseForURL:@"http://tests.example.com"]; + + FIRDatabaseReference *testRef1 = [db1 reference]; + FIRDatabaseReference *testRef2 = [db1 referenceWithPath:@"foo"]; + FIRDatabaseReference *testRef3 = [altDb reference]; + FIRDatabaseReference *testRef4 = [wrongHostDb reference]; + FIRDatabaseReference *testRef5 = [db2 reference]; + FIRDatabaseReference *testRef6 = [db2 reference]; + + // Referential equality + XCTAssertTrue(testRef1.database == testRef2.database); + XCTAssertFalse(testRef1.database == testRef3.database); + XCTAssertFalse(testRef1.database == testRef4.database); + XCTAssertFalse(testRef1.database == testRef5.database); + XCTAssertFalse(testRef1.database == testRef6.database); + + // references from same FIRDatabase same identical .database references. + XCTAssertTrue(testRef5.database == testRef6.database); + + [db1 goOffline]; + [db2 goOffline]; + [altDb goOffline]; + [wrongHostDb goOffline]; +} + +- (FIRDatabaseReference *)rootRefWithEngine:(id<FStorageEngine>)engine name:(NSString *)name { + FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:name]; + config.persistenceEnabled = YES; + config.forceStorageEngine = engine; + return [[FIRDatabaseReference alloc] initWithConfig:config]; +} + +- (void) testPurgeWritesPurgesAllWrites { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesPurgesAllWrites"]; + FIRDatabase *database = ref.database; + + [database goOffline]; + + [[ref childByAutoId] setValue:@"test-value-1"]; + [[ref childByAutoId] setValue:@"test-value-2"]; + [[ref childByAutoId] setValue:@"test-value-3"]; + [[ref childByAutoId] setValue:@"test-value-4"]; + + [self waitForEvents:ref]; + + XCTAssertEqual(engine.userWrites.count, (NSUInteger)4); + + [database purgeOutstandingWrites]; + [self waitForEvents:ref]; + XCTAssertEqual(engine.userWrites.count, (NSUInteger)0); + + [database goOnline]; +} + +- (void) testPurgeWritesAreCanceledInOrder { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesAndCanceledInOrder"]; + FIRDatabase *database = ref.database; + + [database goOffline]; + + NSMutableArray *order = [NSMutableArray array]; + + [[ref childByAutoId] setValue:@"test-value-1" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [order addObject:@"1"]; + }]; + [[ref childByAutoId] setValue:@"test-value-2" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [order addObject:@"2"]; + }]; + [[ref childByAutoId] setValue:@"test-value-3" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [order addObject:@"3"]; + }]; + [[ref childByAutoId] setValue:@"test-value-4" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [order addObject:@"4"]; + }]; + + [self waitForEvents:ref]; + + XCTAssertEqual(engine.userWrites.count, (NSUInteger)4); + + [database purgeOutstandingWrites]; + [self waitForEvents:ref]; + XCTAssertEqual(engine.userWrites.count, (NSUInteger)0); + XCTAssertEqualObjects(order, (@[@"1", @"2", @"3", @"4"])); + + [database goOnline]; +} + +- (void)testPurgeWritesCancelsOnDisconnects { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FIRDatabaseReference *ref = [self rootRefWithEngine:engine name:@"purgeWritesCancelsOnDisconnects"]; + FIRDatabase *database = ref.database; + + [database goOffline]; + + NSMutableArray *events = [NSMutableArray array]; + + [[ref childByAutoId] onDisconnectSetValue:@"test-value-1" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [events addObject:@"1"]; + }]; + + [[ref childByAutoId] onDisconnectSetValue:@"test-value-2" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [events addObject:@"2"]; + }]; + + [self waitForEvents:ref]; + + [database purgeOutstandingWrites]; + + [self waitForEvents:ref]; + + XCTAssertEqualObjects(events, (@[@"1", @"2"])); +} + +- (void) testPurgeWritesReraisesEvents { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FIRDatabaseReference *ref = [[self rootRefWithEngine:engine name:@"purgeWritesReraiseEvents"] childByAutoId]; + FIRDatabase *database = ref.database; + + [self waitForCompletionOf:ref setValue:@{@"foo": @"foo-value", @"bar": @{@"qux": @"qux-value"}}]; + + NSMutableArray *fooValues = [NSMutableArray array]; + NSMutableArray *barQuuValues = [NSMutableArray array]; + NSMutableArray *barQuxValues = [NSMutableArray array]; + NSMutableArray *cancelOrder = [NSMutableArray array]; + + [[ref child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [fooValues addObject:snapshot.value]; + }]; + [[ref child:@"bar/quu"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [barQuuValues addObject:snapshot.value]; + }]; + [[ref child:@"bar/qux"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [barQuxValues addObject:snapshot.value]; + }]; + + [database goOffline]; + + [[ref child:@"foo"] setValue:@"new-foo-value" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + // This should be after we raised events + XCTAssertEqualObjects(fooValues.lastObject, @"foo-value"); + [cancelOrder addObject:@"foo-1"]; + }]; + + [[ref child:@"bar"] updateChildValues:@{@"quu": @"quu-value", @"qux": @"new-qux-value"} + withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + // This should be after we raised events + XCTAssertEqualObjects(barQuxValues.lastObject, @"qux-value"); + XCTAssertEqualObjects(barQuuValues.lastObject, [NSNull null]); + [cancelOrder addObject:@"bar"]; + }]; + + [[ref child:@"foo"] setValue:@"newest-foo-value" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + // This should be after we raised events + XCTAssertEqualObjects(fooValues.lastObject, @"foo-value"); + [cancelOrder addObject:@"foo-2"]; + }]; + + [database purgeOutstandingWrites]; + + [self waitForEvents:ref]; + + XCTAssertEqualObjects(cancelOrder, (@[@"foo-1", @"bar", @"foo-2"])); + XCTAssertEqualObjects(fooValues, (@[@"foo-value", @"new-foo-value", @"newest-foo-value", @"foo-value"])); + XCTAssertEqualObjects(barQuuValues, (@[[NSNull null], @"quu-value", [NSNull null]])); + XCTAssertEqualObjects(barQuxValues, (@[@"qux-value", @"new-qux-value", @"qux-value"])); + + [database goOnline]; + // Make sure we're back online and reconnected again + [self waitForRoundTrip:ref]; + + // No events should be reraised + XCTAssertEqualObjects(cancelOrder, (@[@"foo-1", @"bar", @"foo-2"])); + XCTAssertEqualObjects(fooValues, (@[@"foo-value", @"new-foo-value", @"newest-foo-value", @"foo-value"])); + XCTAssertEqualObjects(barQuuValues, (@[[NSNull null], @"quu-value", [NSNull null]])); + XCTAssertEqualObjects(barQuxValues, (@[@"qux-value", @"new-qux-value", @"qux-value"])); +} + +- (void)testPurgeWritesCancelsTransactions { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FIRDatabaseReference *ref = [[self rootRefWithEngine:engine name:@"purgeWritesCancelsTransactions"] childByAutoId]; + FIRDatabase *database = ref.database; + + NSMutableArray *events = [NSMutableArray array]; + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [events addObject:[NSString stringWithFormat:@"value-%@", snapshot.value]]; + }]; + + // Make sure the first value event is fired + [self waitForRoundTrip:ref]; + + [database goOffline]; + + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"1"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [events addObject:@"cancel-1"]; + }]; + + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"2"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertEqual(error.code, kFErrorCodeWriteCanceled); + [events addObject:@"cancel-2"]; + }]; + + [database purgeOutstandingWrites]; + + [self waitForEvents:ref]; + + // The order should really be cancel-1 then cancel-2, but meh, to difficult to implement currently... + XCTAssertEqualObjects(events, (@[@"value-<null>", @"value-1", @"value-2", @"value-<null>", @"cancel-2", @"cancel-1"])); +} + +- (void) testPersistenceEnabled { + id app = [[FIRFakeApp alloc] initWithName:@"testPersistenceEnabled" URL:self.databaseURL]; + FIRDatabase *database = [FIRDatabase databaseForApp:app]; + database.persistenceEnabled = YES; + XCTAssertTrue(database.persistenceEnabled); + + // Just do a dummy observe that should get null added to the persistent cache. + FIRDatabaseReference *ref = [[database reference] childByAutoId]; + [self waitForValueOf:ref toBe:[NSNull null]]; + + // Now go offline and since null is cached offline, our observer should still complete. + [database goOffline]; + [self waitForValueOf:ref toBe:[NSNull null]]; +} + +- (void) testPersistenceCacheSizeBytes { + id app = [[FIRFakeApp alloc] initWithName:@"testPersistenceCacheSizeBytes" URL:self.databaseURL]; + FIRDatabase *database = [FIRDatabase databaseForApp:app]; + database.persistenceEnabled = YES; + + int oneMegabyte = 1 * 1024 * 1024; + + XCTAssertThrows([database setPersistenceCacheSizeBytes: 1], @"Cache must be a least 1 MB."); + XCTAssertThrows([database setPersistenceCacheSizeBytes: 101 * oneMegabyte], + @"Cache must be less than 100 MB."); + database.persistenceCacheSizeBytes = 2 * oneMegabyte; + XCTAssertEqual(2 * oneMegabyte, database.persistenceCacheSizeBytes); + + [database reference]; // Initialize database. + + XCTAssertThrows([database setPersistenceCacheSizeBytes: 3 * oneMegabyte], + @"Persistence can't be changed after initialization."); + XCTAssertEqual(2 * oneMegabyte, database.persistenceCacheSizeBytes); +} + +- (void) testCallbackQueue { + id app = [[FIRFakeApp alloc] initWithName:@"testCallbackQueue" URL:self.databaseURL]; + FIRDatabase *database = [FIRDatabase databaseForApp:app]; + dispatch_queue_t callbackQueue = dispatch_queue_create("testCallbackQueue", NULL); + database.callbackQueue = callbackQueue; + XCTAssertEqual(callbackQueue, database.callbackQueue); + + __block BOOL done = NO; + [database.reference.childByAutoId observeSingleEventOfType:FIRDataEventTypeValue + withBlock:^(FIRDataSnapshot *snapshot) { + dispatch_assert_queue(callbackQueue); + done = YES; + }]; + WAIT_FOR(done); + [database goOffline]; +} + +- (FIRDatabase *) defaultDatabase { + return [self databaseForURL:self.databaseURL]; +} + +- (FIRDatabase *) databaseForURL:(NSString *)url { + NSString *name = [NSString stringWithFormat:@"url:%@", url]; + return [self databaseForURL:url name:name]; +} + +- (FIRDatabase *) databaseForURL:(NSString *)url name:(NSString *)name { + id app = [[FIRFakeApp alloc] initWithName:name URL:url]; + return [FIRDatabase databaseForApp:app]; +} +@end diff --git a/Example/Database/Tests/Integration/FKeepSyncedTest.m b/Example/Database/Tests/Integration/FKeepSyncedTest.m new file mode 100644 index 0000000..96d5cf8 --- /dev/null +++ b/Example/Database/Tests/Integration/FKeepSyncedTest.m @@ -0,0 +1,230 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +#import "FTestHelpers.h" +#import "FTestBase.h" + +@interface FKeepSyncedTest : FTestBase + +@end + +@implementation FKeepSyncedTest + +static NSUInteger fGlobalKeepSyncedTestCounter = 0; + +- (void)assertIsKeptSynced:(FIRDatabaseQuery *)query { + FIRDatabaseReference *ref = query.ref; + + // First set a unique value to the value of child + fGlobalKeepSyncedTestCounter++; + NSNumber *currentValue = @(fGlobalKeepSyncedTestCounter); + __block BOOL done = NO; + [ref setValue:@{ @"child": currentValue} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertNil(error); + done = YES; + }]; + + WAIT_FOR(done); + done = NO; + + // Next go offline, if it's kept synced we should have kept the value, after going offline no way to get the value + // except from cache + [FIRDatabaseReference goOffline]; + + [query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // We should receive an event + XCTAssertEqualObjects(snapshot.value, @{@"child" : currentValue}); + done = YES; + }]; + + WAIT_FOR(done); + // All good, go back online + [FIRDatabaseReference goOnline]; +} + +- (void)assertNotKeptSynced:(FIRDatabaseQuery *)query { + FIRDatabaseReference *ref = query.ref; + + // First set a unique value to the value of child + fGlobalKeepSyncedTestCounter++; + NSNumber *currentValue = @(fGlobalKeepSyncedTestCounter); + fGlobalKeepSyncedTestCounter++; + NSNumber *newValue = @(fGlobalKeepSyncedTestCounter); + __block BOOL done = NO; + [ref setValue:@{ @"child": currentValue} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertNil(error); + done = YES; + }]; + + WAIT_FOR(done); + done = NO; + + // Next go offline, if it's kept synced we should have kept the value, after going offline no way to get the value + // except from cache + [FIRDatabaseReference goOffline]; + + [query observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // We should receive an event + XCTAssertEqualObjects(snapshot.value, @{@"child" : newValue}); + done = YES; + }]; + + // By now, if we had it synced we should have gotten an event with the wrong value + // Write a new value so the value event listener will be triggered + [ref setValue:@{ @"child": newValue}]; + WAIT_FOR(done); + + // All good, go back online + [FIRDatabaseReference goOnline]; +} + +- (void)testKeepSynced { + FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence]; + + [ref keepSynced:YES]; + [self assertIsKeptSynced:ref]; + + [ref keepSynced:NO]; + [self assertNotKeptSynced:ref]; +} + +- (void)testManyKeepSyncedCallsDontAccumulate { + FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence]; + + [ref keepSynced:YES]; + [ref keepSynced:YES]; + [ref keepSynced:YES]; + [self assertIsKeptSynced:ref]; + + // If it were balanced, this would not be enough + [ref keepSynced:NO]; + [ref keepSynced:NO]; + [self assertNotKeptSynced:ref]; + + // If it were balanced, this would not be enough + [ref keepSynced:YES]; + [self assertIsKeptSynced:ref]; + + // cleanup + [ref keepSynced:NO]; +} + +- (void)testRemoveAllObserversDoesNotAffectKeepSynced { + FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence]; + + [ref keepSynced:YES]; + [self assertIsKeptSynced:ref]; + + [ref removeAllObservers]; + [self assertIsKeptSynced:ref]; + + // cleanup + [ref keepSynced:NO]; +} + +- (void)testRemoveSingleObserverDoesNotAffectKeepSynced { + FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence]; + + [ref keepSynced:YES]; + [self assertIsKeptSynced:ref]; + + __block BOOL done = NO; + FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = YES; + }]; + + WAIT_FOR(done); + [ref removeObserverWithHandle:handle]; + + [self assertIsKeptSynced:ref]; + + // cleanup + [ref keepSynced:NO]; +} + +- (void)testKeepSyncedNoDoesNotAffectExistingObserver { + FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence]; + + [ref keepSynced:YES]; + [self assertIsKeptSynced:ref]; + + __block BOOL done = NO; + FIRDatabaseHandle handle = [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = [snapshot.value isEqual:@"done"]; + }]; + + // cleanup + [ref keepSynced:NO]; + + [ref setValue:@"done"]; + + WAIT_FOR(done); + [ref removeObserverWithHandle:handle]; +} + + +- (void)testDifferentQueriesAreIndependent { + FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence]; + FIRDatabaseQuery *query1 = [ref queryLimitedToFirst:1]; + FIRDatabaseQuery *query2 = [ref queryLimitedToFirst:2]; + + [query1 keepSynced:YES]; + [self assertIsKeptSynced:query1]; + [self assertNotKeptSynced:query2]; + + [query2 keepSynced:YES]; + [self assertIsKeptSynced:query1]; + [self assertIsKeptSynced:query2]; + + [query1 keepSynced:NO]; + [self assertIsKeptSynced:query2]; + [self assertNotKeptSynced:query1]; + + [query2 keepSynced:NO]; + [self assertNotKeptSynced:query1]; + [self assertNotKeptSynced:query2]; + +} + +- (void)testChildIsKeptSynced { + FIRDatabaseReference *ref = [FTestHelpers getRandomNodeWithoutPersistence]; + FIRDatabaseReference *child = [ref child:@"random-child"]; + + [ref keepSynced:YES]; + [self assertIsKeptSynced:child]; + + // cleanup + [ref keepSynced:NO]; +} + +- (void)testRootIsKeptSynced { + FIRDatabaseReference *ref = [[FTestHelpers getRandomNodeWithoutPersistence] root]; + + [ref keepSynced:YES]; + // Run on random child to make sure writes from this test doesn't interfere with any other tests. + [self assertIsKeptSynced:[ref childByAutoId]]; + + // cleanup + [ref keepSynced:NO]; +} + +// TODO[offline]: Cancel listens for keep synced.... + + + +@end diff --git a/Example/Database/Tests/Integration/FOrder.h b/Example/Database/Tests/Integration/FOrder.h new file mode 100644 index 0000000..d39de2a --- /dev/null +++ b/Example/Database/Tests/Integration/FOrder.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FTestBase.h" + +@interface FOrder : FTestBase + +@end diff --git a/Example/Database/Tests/Integration/FOrder.m b/Example/Database/Tests/Integration/FOrder.m new file mode 100644 index 0000000..e8c628b --- /dev/null +++ b/Example/Database/Tests/Integration/FOrder.m @@ -0,0 +1,646 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FOrder.h" +#import "FIRDatabaseReference.h" +#import "FTypedefs_Private.h" +#import "FTupleFirebase.h" +#import "FTestHelpers.h" +#import "FEventTester.h" +#import "FTupleEventTypeString.h" + +@implementation FOrder + +- (void) testPushEnumerateAndCheckCorrectOrder { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + for(int i = 0; i < 10; i++) { + [[node childByAutoId] setValue:[NSNumber numberWithInt:i]]; + } + + [super snapWaiter:node withBlock:^(FIRDataSnapshot * snapshot) { + int expected = 0; + for (FIRDataSnapshot * child in snapshot.children) { + XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expects values match."); + expected = expected + 1; + } + XCTAssertTrue(expected == 10, @"Should get all of the children"); + XCTAssertTrue(expected == snapshot.childrenCount, @"Snapshot should report correct count"); + }]; +} + +- (void) testPushEnumerateManyPathsWriteAndCheckOrder { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + NSMutableArray* paths = [[NSMutableArray alloc] init]; + + for(int i = 0; i < 20; i++) { + [paths addObject:[node childByAutoId]]; + } + + for(int i = 0; i < 20; i++) { + [(FIRDatabaseReference *)[paths objectAtIndex:i] setValue:[NSNumber numberWithInt:i]]; + } + + [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) { + int expected = 0; + for (FIRDataSnapshot * child in snap.children) { + XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expects values match."); + expected = expected + 1; + } + XCTAssertTrue(expected == 20, @"Should get all of the children"); + XCTAssertTrue(expected == snap.childrenCount, @"Snapshot should report correct count"); + }]; +} + +- (void) testPushDataReconnectReadBackAndVerifyOrder { + + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + + __block int expected = 0; + __block int nodesSet = 0; + FIRDatabaseReference * node = tuple.one; + for(int i = 0; i < 10; i++) { + [[node childByAutoId] setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError* err, FIRDatabaseReference * ref) { + nodesSet++; + }]; + } + + [self waitUntil:^BOOL{ + return nodesSet == 10; + }]; + + __block BOOL done = NO; + [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) { + expected = 0; + //[snap forEach:^BOOL(FIRDataSnapshot *child) { + for (FIRDataSnapshot * child in snap.children) { + XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expected child value"); + expected = expected + 1; + //return NO; + } + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + XCTAssertTrue(nodesSet == 10, @"All of the nodes have been set"); + + [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) { + expected = 0; + for (FIRDataSnapshot * child in snap.children) { + XCTAssertEqualObjects([NSNumber numberWithInt:expected], [child value], @"Expected child value"); + expected = expected + 1; + } + done = YES; + XCTAssertTrue(expected == 10, @"Saw the expected number of children %d == 10", expected); + }]; + +} + +- (void) testPushDataWithPrioritiesReconnectReadBackAndVerifyOrder { + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + + __block int expected = 0; + __block int nodesSet = 0; + FIRDatabaseReference * node = tuple.one; + for(int i = 0; i < 10; i++) { + [[node childByAutoId] setValue:[NSNumber numberWithInt:i] andPriority:[NSNumber numberWithInt:(10 - i)] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + nodesSet = nodesSet + 1; + }]; + } + + [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) { + expected = 9; + + for (FIRDataSnapshot * child in snap.children) { + XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority"); + expected = expected - 1; + } + XCTAssertTrue(expected == -1, @"Saw the expected number of children"); + }]; + + [self waitUntil:^BOOL{ + return nodesSet == 10; + }]; + + XCTAssertTrue(nodesSet == 10, @"All of the nodes have been set"); + + [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) { + expected = 9; + for (FIRDataSnapshot * child in snap.children) { + XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority"); + expected = expected - 1; + } + XCTAssertTrue(expected == -1, @"Saw the expected number of children"); + }]; +} + +- (void) testPushDataWithExponentialPrioritiesReconnectReadBackAndVerifyOrder { + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + + __block int expected = 0; + __block int nodesSet = 0; + FIRDatabaseReference * node = tuple.one; + for(int i = 0; i < 10; i++) { + [[node childByAutoId] setValue:[NSNumber numberWithInt:i] andPriority:[NSNumber numberWithDouble:(111111111111111111111111111111.0 / pow(10, i))] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + nodesSet = nodesSet + 1; + }]; + } + + [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) { + expected = 9; + + for (FIRDataSnapshot * child in snap.children) { + XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority"); + expected = expected - 1; + } + XCTAssertTrue(expected == -1, @"Saw the expected number of children"); + }]; + + WAIT_FOR(nodesSet == 10); + + [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) { + expected = 9; + for (FIRDataSnapshot * child in snap.children) { + XCTAssertEqualObjects([child value], [NSNumber numberWithInt:expected], @"Expected child value as per priority"); + expected = expected - 1; + } + XCTAssertTrue(expected == -1, @"Saw the expected number of children"); + }]; +} + +- (void) testThatNodesWithoutValuesAreNotEnumerated { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + [node child:@"foo"]; + [[node child:@"bar"] setValue:@"test"]; + + __block int items = 0; + [super snapWaiter:node withBlock:^(FIRDataSnapshot *snap) { + + for (FIRDataSnapshot * child in snap.children) { + items = items + 1; + XCTAssertEqualObjects([child key], @"bar", @"Saw the child which had a value set and not the empty one"); + } + + XCTAssertTrue(items == 1, @"Saw only the one that was actually set."); + }]; +} + +- (void) testChildMovedEventWhenPriorityChanges { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + FEventTester* et = [[FEventTester alloc] initFrom:self]; + + NSArray* expect = @[ + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"b"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"c"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildMoved withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:expect]; + + [et waitForInitialization]; + + [[node child:@"a"] setValue:@"first" andPriority:@1]; + [[node child:@"b"] setValue:@"second" andPriority:@2]; + [[node child:@"c"] setValue:@"third" andPriority:@3]; + + [[node child:@"a"] setPriority:@15]; + + [et wait]; +} + + +- (void) testCanResetPriorityToNull { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [[node child:@"a"] setValue:@"a" andPriority:@1]; + [[node child:@"b"] setValue:@"b" andPriority:@2]; + + FEventTester* et = [[FEventTester alloc] initFrom:self]; + NSArray* expect = @[ + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"a"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildAdded withString:@"b"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:expect]; + + [et wait]; + + expect = @[ + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildMoved withString:@"b"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeChildChanged withString:@"b"], + [[FTupleEventTypeString alloc] initWithFirebase:node withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:expect]; + + [[node child:@"b"] setPriority:nil]; + + [et wait]; + + __block BOOL ready = NO; + [[node child:@"b"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertTrue([snapshot priority] == [NSNull null], @"Should be null"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testInsertingANodeUnderALeafPreservesItsPriority { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot * snap; + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) { + snap = s; + }]; + + [node setValue:@"a" andPriority:@10]; + [[node child:@"deeper"] setValue:@"deeper"]; + + [self waitUntil:^BOOL{ + id result = [snap value]; + NSDictionary* expected = @{@"deeper": @"deeper"}; + return snap != nil && [result isKindOfClass:[NSDictionary class]] && [result isEqualToDictionary:expected]; + }]; + + XCTAssertEqualObjects([snap priority], @10, @"Proper value"); +} + +- (void) testVerifyOrderOfMixedNumbersStringNoPriorities { + FTupleFirebase* tuple = [FTestHelpers getRandomNodePair]; + + NSArray* nodeAndPriorities = @[ + @"alpha42", @"zed", + @"noPriorityC", [NSNull null], + @"num41", @500, + @"noPriorityB", [NSNull null], + @"num80", @4000.1, + @"num50", @4000, + @"num10", @24, + @"alpha41", @"zed", + @"alpha20", @"horse", + @"num20", @123, + @"num70", @4000.01, + @"noPriorityA", [NSNull null], + @"alpha30", @"tree", + @"num30", @300, + @"num60", @4000.001, + @"alpha10", @"0horse", + @"num42", @500, + @"alpha40", @"zed", + @"num40", @500 + ]; + + __block int setsCompleted = 0; + + for (int i = 0; i < [nodeAndPriorities count]; i++) { + FIRDatabaseReference * n = [tuple.one child:[nodeAndPriorities objectAtIndex:i++]]; + [n setValue:@1 andPriority:[nodeAndPriorities objectAtIndex:i] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + setsCompleted = setsCompleted + 1; + }]; + } + + NSString* expected = @"noPriorityA, noPriorityB, noPriorityC, num10, num20, num30, num40, num41, num42, num50, num60, num70, num80, alpha10, alpha20, alpha30, alpha40, alpha41, alpha42, "; + + [super snapWaiter:tuple.one withBlock:^(FIRDataSnapshot *snap) { + NSMutableString* output = [[NSMutableString alloc] init]; + for (FIRDataSnapshot * n in snap.children) { + [output appendFormat:@"%@, ", [n key]]; + } + + XCTAssertTrue([expected isEqualToString:output], @"Proper order"); + }]; + + WAIT_FOR(setsCompleted == [nodeAndPriorities count] / 2); + + [super snapWaiter:tuple.two withBlock:^(FIRDataSnapshot *snap) { + NSMutableString* output = [[NSMutableString alloc] init]; + for (FIRDataSnapshot * n in snap.children) { + [output appendFormat:@"%@, ", [n key]]; + } + + XCTAssertTrue([expected isEqualToString:output], @"Proper order"); + }]; +} + +- (void) testVerifyOrderOfIntegerNames { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + NSArray* keys = @[ + @"foo", + @"bar", + @"03", + @"0", + @"100", + @"20", + @"5", + @"3", + @"003", + @"9" + ]; + + __block int setsCompleted = 0; + + for (int i = 0; i < [keys count]; i++) { + FIRDatabaseReference * n = [ref child:[keys objectAtIndex:i]]; + [n setValue:@1 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + setsCompleted = setsCompleted + 1; + }]; + } + + NSString* expected = @"0, 3, 03, 003, 5, 9, 20, 100, bar, foo, "; + + [super snapWaiter:ref withBlock:^(FIRDataSnapshot *snap) { + NSMutableString* output = [[NSMutableString alloc] init]; + for (FIRDataSnapshot * n in snap.children) { + [output appendFormat:@"%@, ", [n key]]; + } + + XCTAssertTrue([expected isEqualToString:output], @"Proper order"); + }]; +} + +- (void) testPrevNameIsCorrectOnChildAddedEvent { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [node setValue:@{@"a": @1, @"b": @2, @"c": @3}]; + + + NSMutableString* added = [[NSMutableString alloc] init]; + + __block int count = 0; + [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) { + [added appendFormat:@"%@ %@, ", [snap key], prevName]; + count++; + }]; + + [self waitUntil:^BOOL{ + return count == 3; + }]; + + XCTAssertTrue([added isEqualToString:@"a (null), b a, c b, "], @"Proper order and prevname"); + +} + +- (void) testPrevNameIsCorrectWhenAddingNewNodes { + + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [node setValue:@{@"b": @2, @"c": @3, @"d": @4}]; + + NSMutableString* added = [[NSMutableString alloc] init]; + + __block int count = 0; + [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) { + [added appendFormat:@"%@ %@, ", [snap key], prevName]; + count++; + }]; + + [self waitUntil:^BOOL{ + return count == 3; + }]; + + XCTAssertTrue([added isEqualToString:@"b (null), c b, d c, "], @"Proper order and prevname"); + + [added setString:@""]; + [[node child:@"a"] setValue:@1]; + [self waitUntil:^BOOL{ + return count == 4; + }]; + + XCTAssertTrue([added isEqualToString:@"a (null), "], @"Proper insertion of new node"); + + [added setString:@""]; + [[node child:@"e"] setValue:@5]; + [self waitUntil:^BOOL{ + return count == 5; + }]; + XCTAssertTrue([added isEqualToString:@"e d, "], @"Proper insertion of new node"); +} + +- (void) testPrevNameIsCorrectWhenAddingNewNodesWithJSON { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + [node setValue:@{@"b": @2, @"c": @3, @"d": @4}]; + + NSMutableString* added = [[NSMutableString alloc] init]; + __block int count = 0; + [node observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snap, NSString *prevName) { + [added appendFormat:@"%@ %@, ", [snap key], prevName]; + count++; + }]; + + [self waitUntil:^BOOL{ + return count == 3; + }]; + + XCTAssertTrue([added isEqualToString:@"b (null), c b, d c, "], @"Proper order and prevname"); + + [added setString:@""]; + [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4}]; + [self waitUntil:^BOOL{ + return count == 4; + }]; + + XCTAssertTrue([added isEqualToString:@"a (null), "], @"Proper insertion of new node"); + + [added setString:@""]; + [node setValue:@{@"a": @1, @"b": @2, @"c": @3, @"d": @4, @"e": @5}]; + [self waitUntil:^BOOL{ + return count == 5; + }]; + + XCTAssertTrue([added isEqualToString:@"e d, "], @"Proper insertion of new node"); +} + +- (void) testPrevNameIsCorrectWhenMovingNodes { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSMutableString* moved = [[NSMutableString alloc] init]; + + __block int count = 0; + [node observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + [moved appendFormat:@"%@ %@, ", snapshot.key, prevName]; + count++; + }]; + + [[node child:@"a"] setValue:@"a" andPriority:@1]; + [[node child:@"b"] setValue:@"a" andPriority:@2]; + [[node child:@"c"] setValue:@"a" andPriority:@3]; + [[node child:@"d"] setValue:@"a" andPriority:@4]; + + [[node child:@"d"] setPriority:@0]; + [self waitUntil:^BOOL{ + return count == 1; + }]; + + XCTAssertTrue([moved isEqualToString:@"d (null), "], @"Got first move"); + + [moved setString:@""]; + [[node child:@"a"] setPriority:@4]; + [self waitUntil:^BOOL{ + return count == 2; + }]; + + XCTAssertTrue([moved isEqualToString:@"a c, "], @"Got second move"); + + [moved setString:@""]; + [[node child:@"c"] setPriority:@0.5]; + [self waitUntil:^BOOL{ + return count == 3; + }]; + + XCTAssertTrue([moved isEqualToString:@"c d, "], @"Got third move"); +} + + +- (void) testPrevNameIsCorrectWhenSettingWholeJsonDict { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + NSMutableString* moved = [[NSMutableString alloc] init]; + + __block int count = 0; + [node observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + [moved appendFormat:@"%@ %@, ", snapshot.key, prevName]; + count++; + }]; + + [node setValue:@{ + @"a": @{@".value": @"a", @".priority": @1}, + @"b": @{@".value": @"b", @".priority": @2}, + @"c": @{@".value": @"c", @".priority": @3}, + @"d": @{@".value": @"d", @".priority": @4} + }]; + + [node setValue:@{ + @"d": @{@".value": @"d", @".priority": @0}, + @"a": @{@".value": @"a", @".priority": @1}, + @"b": @{@".value": @"b", @".priority": @2}, + @"c": @{@".value": @"c", @".priority": @3} + }]; + [self waitUntil:^BOOL{ + return count == 1; + }]; + + XCTAssertTrue([moved isEqualToString:@"d (null), "], @"Got move"); + + [moved setString:@""]; + + [node setValue:@{ + @"d": @{@".value": @"d", @".priority": @0}, + @"b": @{@".value": @"b", @".priority": @2}, + @"c": @{@".value": @"c", @".priority": @3}, + @"a": @{@".value": @"a", @".priority": @4} + }]; + + [self waitUntil:^BOOL{ + return count == 2; + }]; + + XCTAssertTrue([moved isEqualToString:@"a c, "], @"Got move"); + + [moved setString:@""]; + + [node setValue:@{ + @"d": @{@".value": @"d", @".priority": @0}, + @"c": @{@".value": @"c", @".priority": @0.5}, + @"b": @{@".value": @"b", @".priority": @2}, + @"a": @{@".value": @"a", @".priority": @4} + }]; + + [self waitUntil:^BOOL{ + return count == 3; + }]; + + XCTAssertTrue([moved isEqualToString:@"c d, "], @"Got move"); +} + +- (void) testCase595NoChildMovedEventWhenDeletingPrioritizedGrandchild { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block int moves = 0; + [node observeEventType:FIRDataEventTypeChildMoved withBlock:^(FIRDataSnapshot *snapshot) { + moves++; + }]; + + __block BOOL ready = NO; + [[node child:@"test/foo"] setValue:@42 andPriority:@"5"]; + [[node child:@"test/foo2"] setValue:@42 andPriority:@"10"]; + [[node child:@"test/foo"] removeValue]; + [[node child:@"test/foo"] removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + XCTAssertTrue(moves == 0, @"Nothing should have moved"); + +} + +- (void) testCanSetAValueWithPriZero { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot * snap = nil; + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) { + snap = s; + }]; + + [node setValue:@"test" andPriority:@0]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertEqualObjects([snap value], @"test", @"Proper value"); + XCTAssertEqualObjects([snap priority], @0, @"Proper value"); +} + +- (void) testCanSetObjectWithPriZero { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot * snap = nil; + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *s) { + snap = s; + }]; + + [node setValue:@{@"x": @"test", @"y": @7} andPriority:@0]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertEqualObjects([[snap value] objectForKey:@"x"], @"test", @"Proper value"); + XCTAssertEqualObjects([[snap value] objectForKey:@"y"], @7, @"Proper value"); + XCTAssertEqualObjects([snap priority], @0, @"Proper value"); +} + +@end diff --git a/Example/Database/Tests/Integration/FOrderByTests.h b/Example/Database/Tests/Integration/FOrderByTests.h new file mode 100644 index 0000000..ce7b6f6 --- /dev/null +++ b/Example/Database/Tests/Integration/FOrderByTests.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import "FTestBase.h" + + +@interface FOrderByTests : FTestBase +@end diff --git a/Example/Database/Tests/Integration/FOrderByTests.m b/Example/Database/Tests/Integration/FOrderByTests.m new file mode 100644 index 0000000..aea6b47 --- /dev/null +++ b/Example/Database/Tests/Integration/FOrderByTests.m @@ -0,0 +1,671 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FOrderByTests.h" + +@interface FOrderByTests () +@end + +@implementation FOrderByTests + + +- (void) testCanDefineAndUseAnIndex { + __block FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSArray *users = @[ + @{@"name": @"Andrew", @"nuggets": @35}, + @{@"name": @"Rob", @"nuggets": @40}, + @{@"name": @"Greg", @"nuggets": @38} + ]; + + __block int setCount = 0; + [users enumerateObjectsUsingBlock:^(NSDictionary *user, NSUInteger idx, BOOL *stop) { + [[ref childByAutoId] setValue:user withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + setCount++; + }]; + }]; + + [self waitUntil:^BOOL{ + return setCount == users.count; + }]; + + __block NSMutableArray *byNuggets = [[NSMutableArray alloc] init]; + [[ref queryOrderedByChild:@"nuggets"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *user = snapshot.value; + [byNuggets addObject:user[@"name"]]; + }]; + + [self waitUntil:^BOOL{ + return byNuggets.count == users.count; + }]; + + NSArray *expected = @[@"Andrew", @"Greg", @"Rob"]; + XCTAssertEqualObjects(byNuggets, expected, @"Correct by-nugget ordering."); +} + +- (void) testCanDefineAndUseDeepIndex { + __block FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSArray *users = @[ + @{@"name": @"Andrew", @"deep": @{@"nuggets": @35}}, + @{@"name": @"Rob", @"deep": @{@"nuggets": @40}}, + @{@"name": @"Greg", @"deep": @{@"nuggets": @38}} + ]; + + __block int setCount = 0; + [users enumerateObjectsUsingBlock:^(NSDictionary *user, NSUInteger idx, BOOL *stop) { + [[ref childByAutoId] setValue:user withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + setCount++; + }]; + }]; + + [self waitUntil:^BOOL{ + return setCount == users.count; + }]; + + __block NSMutableArray *byNuggets = [[NSMutableArray alloc] init]; + [[ref queryOrderedByChild:@"deep/nuggets"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *user = snapshot.value; + [byNuggets addObject:user[@"name"]]; + }]; + + [self waitUntil:^BOOL{ + return byNuggets.count == users.count; + }]; + + NSArray *expected = @[@"Andrew", @"Greg", @"Rob"]; + XCTAssertEqualObjects(byNuggets, expected, @"Correct by-nugget ordering."); +} + +- (void) testCanUsaAFallbackThenDefineTheSpecifiedIndex { + FTupleFirebase *tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference *reader = tuple.one, *writer = tuple.two; + + NSDictionary *foo1 = @{ + @"a" : @{@"order" : @2, @"foo" : @1}, + @"b" : @{@"order" : @0}, + @"c" : @{@"order" : @1, @"foo" : @NO}, + @"d" : @{@"order" : @3, @"foo" : @"hello"} + }; + + NSDictionary *foo_e = @{@"order": @1.5, @"foo": @YES}; + NSDictionary *foo_f = @{@"order": @4, @"foo": @{@"bar": @"baz"}}; + + [self waitForCompletionOf:writer setValue:foo1]; + + NSMutableArray *snaps = [[NSMutableArray alloc] init]; + [[[reader queryOrderedByChild:@"order"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [snaps addObject:snapshot.value]; + }]; + WAIT_FOR(snaps.count == 1); + + NSDictionary *expected = @{ + @"d": @{@"order": @3, @"foo": @"hello"}, + @"a": @{@"order": @2, @"foo": @1} + }; + XCTAssertEqualObjects(snaps[0], expected, @"Got correct result"); + + + [self waitForCompletionOf:[writer child:@"e"] setValue:foo_e]; + + [self waitForRoundTrip:reader]; + NSLog(@"snaps: %@", snaps); + NSLog(@"snaps.count: %ld", (unsigned long) snaps.count); + XCTAssertEqual(snaps.count, (NSUInteger)1, @"Should still have one event."); + + [self waitForCompletionOf:[writer child:@"f"] setValue:foo_f]; + + [self waitForRoundTrip:reader]; + XCTAssertEqual(snaps.count, (NSUInteger)2, @"Should have gotten another event."); + expected = @{ + @"f": foo_f, + @"d": @{@"order": @3, @"foo": @"hello"} + }; + XCTAssertEqualObjects(snaps[1], expected, @"Correct event."); +} + +- (void) testSnapshotsAreIteratedInOrder { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSDictionary *initial = @{ + @"alex": @{@"nuggets": @60}, + @"rob": @{@"nuggets": @56}, + @"vassili": @{@"nuggets": @55.5}, + @"tony": @{@"nuggets": @52}, + @"greg": @{@"nuggets": @52} + }; + + NSArray *expectedOrder = @[@"greg", @"tony", @"vassili", @"rob", @"alex"]; + NSArray *expectedPrevNames = @[[NSNull null], @"greg", @"tony", @"vassili", @"rob"]; + + NSMutableArray *valueOrder = [[NSMutableArray alloc] init]; + NSMutableArray *addedOrder = [[NSMutableArray alloc] init]; + NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init]; + + FIRDatabaseQuery *orderedRef = [ref queryOrderedByChild:@"nuggets"]; + + [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + [valueOrder addObject:child.key]; + } + }]; + + [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + [addedOrder addObject:snapshot.key]; + [addedPrevNames addObject:prevName ? prevName : [NSNull null]]; + }]; + + [ref setValue:initial]; + WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count); + + XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order."); + XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events."); + XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order."); +} + +- (void) testSnapshotsAreIteratedInOrderForValueIndex { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSDictionary *initial = @{ + @"alex": @60, + @"rob": @56, + @"vassili": @55.5, + @"tony": @52, + @"greg": @52 + }; + + NSArray *expectedOrder = @[@"greg", @"tony", @"vassili", @"rob", @"alex"]; + NSArray *expectedPrevNames = @[[NSNull null], @"greg", @"tony", @"vassili", @"rob"]; + + NSMutableArray *valueOrder = [[NSMutableArray alloc] init]; + NSMutableArray *addedOrder = [[NSMutableArray alloc] init]; + NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init]; + + FIRDatabaseQuery *orderedRef = [ref queryOrderedByValue]; + + [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + [valueOrder addObject:child.key]; + } + }]; + + [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + [addedOrder addObject:snapshot.key]; + [addedPrevNames addObject:prevName ? prevName : [NSNull null]]; + }]; + + [ref setValue:initial]; + WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count); + + XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order."); + XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events."); + XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order."); +} + +- (void) testFiresChildMovedEvents { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSDictionary *initial = @{ + @"alex": @{@"nuggets": @60}, + @"rob": @{@"nuggets": @56}, + @"vassili": @{@"nuggets": @55.5}, + @"tony": @{@"nuggets": @52}, + @"greg": @{@"nuggets": @52} + }; + + FIRDatabaseQuery *orderedRef = [ref queryOrderedByChild:@"nuggets"]; + + __block BOOL moved = NO; + [orderedRef observeEventType:FIRDataEventTypeChildMoved andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + moved = YES; + XCTAssertEqualObjects(snapshot.key, @"greg", @""); + XCTAssertEqualObjects(prevName, @"rob", @""); + XCTAssertEqualObjects(snapshot.value, @{@"nuggets" : @57}, @""); + }]; + + [ref setValue:initial]; + [[ref child:@"greg/nuggets"] setValue:@57]; + WAIT_FOR(moved); +} + +- (void) testDefineMultipleIndexesAtALocation { + FTupleFirebase *tuple = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference *reader = tuple.one, *writer = tuple.two; + + NSDictionary *foo1 = @{ + @"a" : @{@"order" : @2, @"foo" : @2}, + @"b" : @{@"order" : @0}, + @"c" : @{@"order" : @1, @"foo" : @NO}, + @"d" : @{@"order" : @3, @"foo" : @"hello"} + }; + + + [self waitForCompletionOf:writer setValue:foo1]; + + FIRDatabaseQuery *fooOrder = [reader queryOrderedByChild:@"foo"]; + FIRDatabaseQuery *orderOrder = [reader queryOrderedByChild:@"order"]; + NSMutableArray *fooSnaps = [[NSMutableArray alloc] init]; + NSMutableArray *orderSnaps = [[NSMutableArray alloc] init]; + + [[[fooOrder queryStartingAtValue:nil] queryEndingAtValue:@1] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [fooSnaps addObject:snapshot.value]; + }]; + + [[orderOrder queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [orderSnaps addObject:snapshot.value]; + }]; + + WAIT_FOR(fooSnaps.count == 1 && orderSnaps.count == 1); + + NSDictionary *expected = @{ + @"b": @{@"order": @0}, + @"c": @{@"order": @1, @"foo": @NO} + }; + XCTAssertEqualObjects(fooSnaps[0], expected, @""); + + expected = @{ + @"d": @{@"order": @3, @"foo": @"hello"}, + @"a": @{@"order": @2, @"foo": @2}, + }; + XCTAssertEqualObjects(orderSnaps[0], expected, @""); + + [[writer child:@"a"] setValue:@{ + @"order": @-1, @"foo": @1 + }]; + + WAIT_FOR(fooSnaps.count == 2 && orderSnaps.count == 2); + + expected = @{ + @"a": @{@"order": @-1, @"foo": @1 }, + @"b": @{@"order": @0}, + @"c": @{@"order": @1, @"foo": @NO} + }; + XCTAssertEqualObjects(fooSnaps[1], expected, @""); + + expected = @{ + @"d": @{@"order": @3, @"foo": @"hello"}, + @"c": @{@"order": @1, @"foo": @NO} + }; + XCTAssertEqualObjects(orderSnaps[1], expected, @""); +} + +- (void) testCallbackRemovalWorks { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + __block int reads = 0; + FIRDatabaseHandle fooHandle, bazHandle; + fooHandle = [[ref queryOrderedByChild:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + reads++; + }]; + + [[ref queryOrderedByChild:@"bar"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + reads++; + }]; + + bazHandle = [[ref queryOrderedByChild:@"baz"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + reads++; + }]; + + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + reads++; + }]; + + [self waitForCompletionOf:ref setValue:@1]; + + XCTAssertEqual(reads, 4, @""); + + [ref removeObserverWithHandle:fooHandle]; + [self waitForCompletionOf:ref setValue:@2]; + XCTAssertEqual(reads, 7, @""); + + // should be a no-op, resulting in 3 more reads. + [[ref queryOrderedByChild:@"foo"] removeObserverWithHandle:bazHandle]; + [self waitForCompletionOf:ref setValue:@3]; + XCTAssertEqual(reads, 10, @""); + + [[ref queryOrderedByChild:@"bar"] removeAllObservers]; + [self waitForCompletionOf:ref setValue:@4]; + XCTAssertEqual(reads, 12, @""); + + // Now, remove everything. + [ref removeAllObservers]; + [self waitForCompletionOf:ref setValue:@5]; + XCTAssertEqual(reads, 12, @""); +} + +- (void) testChildAddedEventsAreInTheCorrectOrder { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSDictionary *initial = @{ + @"a": @{@"value": @5}, + @"c": @{@"value": @3} + }; + + NSMutableArray *added = [[NSMutableArray alloc] init]; + [[ref queryOrderedByChild:@"value"] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [added addObject:snapshot.key]; + }]; + [ref setValue:initial]; + + WAIT_FOR(added.count == 2); + NSArray *expected = @[@"c", @"a"]; + XCTAssertEqualObjects(added, expected, @""); + + [ref updateChildValues:@{ + @"b": @{@"value": @4}, + @"d": @{@"value": @2} + }]; + + WAIT_FOR(added.count == 4); + expected = @[@"c", @"a", @"d", @"b"]; + XCTAssertEqualObjects(added, expected, @""); +} + +- (void) testCanUseKeyIndex { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSDictionary *data = @{ + @"a": @{ @".priority": @10, @".value": @"a" }, + @"b": @{ @".priority": @5, @".value": @"b" }, + @"c": @{ @".priority": @20, @".value": @"c" }, + @"d": @{ @".priority": @7, @".value": @"d" }, + @"e": @{ @".priority": @30, @".value": @"e" }, + @"f": @{ @".priority": @8, @".value": @"f" } + }; + + [self waitForCompletionOf:ref setValue:data]; + + __block BOOL valueDone = NO; + [[[ref queryOrderedByKey] queryStartingAtValue:@"c"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSMutableArray *keys = [[NSMutableArray alloc] init]; + for (FIRDataSnapshot *child in snapshot.children) { + [keys addObject:child.key]; + } + NSArray *expected = @[@"c", @"d", @"e", @"f"]; + XCTAssertEqualObjects(keys, expected, @""); + valueDone = YES; + }]; + WAIT_FOR(valueDone); + + NSMutableArray *keys = [[NSMutableArray alloc] init]; + [[[ref queryOrderedByKey] queryLimitedToLast:5] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + [keys addObject:child.key]; + } + }]; + + WAIT_FOR(keys.count == 5); + NSArray *expected = @[@"b", @"c", @"d", @"e", @"f"]; + XCTAssertEqualObjects(keys, expected, @""); +} + +- (void) testQueriesWorkOnLeafNodes { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + [self waitForCompletionOf:ref setValue:@"leaf-node"]; + + __block BOOL valueDone = NO; + [[[ref queryOrderedByChild:@"foo"] queryLimitedToLast:1] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqual(snapshot.value, [NSNull null]); + valueDone = YES; + }]; + WAIT_FOR(valueDone); +} + +- (void) testUpdatesForUnindexedQuery { + FTupleFirebase *refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference *reader = refs.one; + FIRDatabaseReference *writer = refs.two; + + __block BOOL done = NO; + NSDictionary *value = @{ @"one": @{ @"index": @1, @"value": @"one" }, + @"two": @{ @"index": @2, @"value": @"two" }, + @"three": @{ @"index": @3, @"value": @"three" } }; + [writer setValue:value withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + WAIT_FOR(done); + + done = NO; + + NSMutableArray *snapshots = [NSMutableArray array]; + + [[[reader queryOrderedByChild:@"index"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [snapshots addObject:snapshot.value]; + done = YES; + }]; + + WAIT_FOR(done); + + NSDictionary *expected = @{ @"two": @{ @"index": @2, @"value": @"two" }, + @"three": @{ @"index": @3, @"value": @"three" } }; + + XCTAssertEqual(snapshots.count, (NSUInteger)1); + XCTAssertEqualObjects(snapshots[0], expected); + + done = NO; + [[writer child:@"one/index"] setValue:@4]; + + WAIT_FOR(done); + + expected = @{ @"one": @{ @"index": @4, @"value": @"one" }, + @"three": @{ @"index": @3, @"value": @"three" } }; + XCTAssertEqual(snapshots.count, (NSUInteger)2); + XCTAssertEqualObjects(snapshots[1], expected); +} + +- (void) testServerRespectsKeyIndex { + FTupleFirebase *refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference *writer = refs.one; + FIRDatabaseReference *reader = refs.two; + + NSDictionary *initial = @{ + @"a": @1, + @"b": @2, + @"c": @3 + }; + + // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected + // and actual data don't match + FIRDatabaseQuery *query = [[[reader queryOrderedByKey] queryStartingAtValue:@"b"] queryLimitedToFirst:2]; + + NSArray *expectedChildren = @[@"b", @"c"]; + + [self waitForCompletionOf:writer setValue:initial]; + + NSMutableArray *children = [[NSMutableArray alloc] init]; + + __block BOOL done = NO; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + [children addObject:child.key]; + } + done = YES; + }]; + + WAIT_FOR(done); + + XCTAssertEqualObjects(expectedChildren, children, @"Got correct children"); +} + +- (void) testServerRespectsValueIndex { + FTupleFirebase *refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference *writer = refs.one; + FIRDatabaseReference *reader = refs.two; + + NSDictionary *initial = @{ + @"a": @1, + @"c": @2, + @"b": @3 + }; + + // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected + // and actual data don't match + FIRDatabaseQuery *query = [[[reader queryOrderedByValue] queryStartingAtValue:@2] queryLimitedToFirst:2]; + + NSArray *expectedChildren = @[@"c", @"b"]; + + [self waitForCompletionOf:writer setValue:initial]; + + NSMutableArray *children = [[NSMutableArray alloc] init]; + + __block BOOL done = NO; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + [children addObject:child.key]; + } + done = YES; + }]; + + WAIT_FOR(done); + + XCTAssertEqualObjects(expectedChildren, children, @"Got correct children"); +} + +- (void) testDeepUpdatesWorkWithQueries { + FTupleFirebase *refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference *writer = refs.one; + FIRDatabaseReference *reader = refs.two; + + + NSDictionary *initial = @{@"a": @{@"data": @"foo", + @"idx": @YES}, + @"b": @{@"data": @"bar", + @"idx": @YES}, + @"c": @{@"data": @"baz", + @"idx": @NO}}; + [self waitForCompletionOf:writer setValue:initial]; + + FIRDatabaseQuery *query = [[reader queryOrderedByChild:@"idx"] queryEqualToValue:@YES]; + + NSDictionary* expected = @{@"a": @{@"data": @"foo", + @"idx": @YES}, + @"b": @{@"data": @"bar", + @"idx": @YES}}; + + [self waitForExportValueOf:query toBe:expected]; + + NSDictionary *update = @{@"a/idx": @NO, + @"b/data": @"blah", + @"c/idx": @YES}; + [self waitForCompletionOf:writer updateChildValues:update]; + + expected = @{@"b": @{@"data": @"blah", + @"idx": @YES}, + @"c": @{@"data": @"baz", + @"idx": @YES}}; + [self waitForExportValueOf:query toBe:expected]; +} + +- (void) testServerRespectsDeepIndex { + FTupleFirebase *refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference *writer = refs.one; + FIRDatabaseReference *reader = refs.two; + + + NSDictionary *initial = @{ + @"a": @{@"deep":@{@"index":@1}}, + @"c": @{@"deep":@{@"index":@2}}, + @"b": @{@"deep":@{@"index":@3}} + }; + + // If the server doesn't respect the index, it will send down limited data, but with no offset, so the expected + // and actual data don't match + FIRDatabaseQuery *query = [[[reader queryOrderedByChild:@"deep/index"] queryStartingAtValue:@2] queryLimitedToFirst:2]; + + NSArray *expectedChildren = @[@"c", @"b"]; + + [self waitForCompletionOf:writer setValue:initial]; + + NSMutableArray *children = [[NSMutableArray alloc] init]; + + __block BOOL done = NO; + [query observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + [children addObject:child.key]; + } + done = YES; + }]; + + WAIT_FOR(done); + + XCTAssertEqualObjects(expectedChildren, children, @"Got correct children"); +} + +- (void) testStartAtEndAtWorksWithValueIndex { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSDictionary *initial = @{ + @"alex": @60, + @"rob": @56, + @"vassili": @55.5, + @"tony": @52, + @"greg": @52 + }; + + NSArray *expectedOrder = @[@"tony", @"vassili", @"rob"]; + NSArray *expectedPrevNames = @[[NSNull null], @"tony", @"vassili"]; + + NSMutableArray *valueOrder = [[NSMutableArray alloc] init]; + NSMutableArray *addedOrder = [[NSMutableArray alloc] init]; + NSMutableArray *addedPrevNames = [[NSMutableArray alloc] init]; + + FIRDatabaseQuery *orderedRef = [[[ref queryOrderedByValue] queryStartingAtValue:@52 childKey:@"tony"] queryEndingAtValue:@59]; + + [orderedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + for (FIRDataSnapshot *child in snapshot.children) { + [valueOrder addObject:child.key]; + } + }]; + + [orderedRef observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + [addedOrder addObject:snapshot.key]; + [addedPrevNames addObject:prevName ? prevName : [NSNull null]]; + }]; + + [ref setValue:initial]; + WAIT_FOR(addedOrder.count == expectedOrder.count && valueOrder.count == expectedOrder.count); + + XCTAssertEqualObjects(addedOrder, expectedOrder, @"child_added events in correct order."); + XCTAssertEqualObjects(addedPrevNames, expectedPrevNames, @"Got correct prevnames for child_added events."); + XCTAssertEqualObjects(valueOrder, expectedOrder, @"enumerated snapshot children in correct order."); +} + +- (void) testRemovingDefaultListenerRemovesNonDefaultListenWithLoadsAllData { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + + NSDictionary *initialData = @{ @"key": @"value" }; + [self waitForCompletionOf:ref setValue:initialData]; + + [[ref queryOrderedByKey] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + }]; + + // Should remove both listener and should remove the listen sent to the server + [ref removeAllObservers]; + + __block id result = nil; + // This used to crash because a listener for [ref queryOrderedByKey] existed already + [[ref queryOrderedByKey] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + result = snapshot.value; + }]; + + WAIT_FOR(result); + XCTAssertEqualObjects(result, initialData); +} + +@end diff --git a/Example/Database/Tests/Integration/FPersist.h b/Example/Database/Tests/Integration/FPersist.h new file mode 100644 index 0000000..5bdfff5 --- /dev/null +++ b/Example/Database/Tests/Integration/FPersist.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import "FTestBase.h" + +@interface FPersist : FTestBase + +@end diff --git a/Example/Database/Tests/Integration/FPersist.m b/Example/Database/Tests/Integration/FPersist.m new file mode 100644 index 0000000..2326e08 --- /dev/null +++ b/Example/Database/Tests/Integration/FPersist.m @@ -0,0 +1,489 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import <Foundation/Foundation.h> +#import "FPersist.h" +#import "FIRDatabaseReference.h" +#import "FIRDatabaseReference_Private.h" +#import "FRepo_Private.h" +#import "FTestHelpers.h" +#import "FDevice.h" +#import "FIRDatabaseQuery_Private.h" + +@implementation FPersist + +- (void) setUp { + [super setUp]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSString *baseDir = [FPersist getFirebaseDir]; + // HACK: We want to clean up old persistence files from previous test runs, but on OSX, baseDir is going to be something + // like /Users/michael/Documents/firebase, and we probably shouldn't blindly delete it, since somebody might have actual + // documents there. We should probably change the directory where we store persistence on OSX to .firebase or something + // to avoid colliding with real files, but for now, we'll leave it and just manually delete each of the /0, /1, /2, etc. + // directories that may exist from previous test runs. As of now (2014/09/07), these directories only go up to ~50, but + // if we add a ton more tests, we may need to increase the 100. But I'm guessing we'll rewrite persistence and move the + // persistence folder before then though. + for(int i = 0; i < 100; i++) { + // TODO: This hack is uneffective because the format now follows different rules. Persistence really needs a purge + // option + NSString *dir = [NSString stringWithFormat:@"%@/%d", baseDir, i]; + if ([fileManager fileExistsAtPath:dir]) { + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:dir error:&error]; + if (error) { + XCTFail(@"Failed to clear persisted data at %@: %@", dir, error); + } + } + } +} + +- (void) testSetIsResentAfterRestart { + FIRDatabaseReference *readerRef = [FTestHelpers getRandomNode]; + NSString *url = [readerRef description]; + FDevice* device = [[FDevice alloc] initOfflineWithUrl:url]; + + // Monitor the data at this location. + __block FIRDataSnapshot *readSnapshot = nil; + [readerRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + readSnapshot = snapshot; + }]; + + // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase. + [device do:^(FIRDatabaseReference *ref) { + [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello", @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri"} }]; + [[ref child:@"a"] setValue:@"a-val"]; + [[ref child:@"c"] setPriority:@"c-pri"]; + [ref updateChildValues:@{ @"b": @"b-val"}]; + }]; + + // restart and wait for "idle" (so all pending puts should have been sent). + [device restartOnline]; + [device waitForIdleUsingWaiter:self]; + + // Pending sets should have gone through. + id expected = @{ + @"a": @"a-val", + @"b": @"b-val", + @"c": @{ @".value": @"hello", @".priority": @"c-pri" }, + @"d": @{ @"dd": @"dd-val", @".priority": @"d-pri" } + }; + [self waitForExportValueOf:readerRef toBe:expected]; + + // Set the value to something else (12). + [readerRef setValue:@12]; + + // "restart" the app again and make sure it doesn't set it to 42 again. + [device restartOnline]; + [device waitForIdleUsingWaiter:self]; + + // Make sure data is still 12. + [self waitForRoundTrip:readerRef]; + XCTAssertEqual(readSnapshot.value, @12, @"Read data should still be 12."); + [device dispose]; +} + +- (void) testSetIsReappliedAfterRestart { + FDevice* device = [[FDevice alloc] initOffline]; + + // Do some sets while offline and then "kill" the app, so it doesn't get sent to Firebase. + [device do:^(FIRDatabaseReference *ref) { + [ref setValue:@{ @"a": @42, @"b": @3.1415, @"c": @"hello" }]; + [[ref child:@"a"] setValue:@"a-val"]; + [[ref child:@"c"] setPriority:@"c-pri"]; + [ref updateChildValues:@{ @"b": @"b-val"}]; + }]; + + // restart the app offline and observe the data. + [device restartOffline]; + + // Pending sets should be visible + id expected = @{ + @"a": @"a-val", + @"b": @"b-val", + @"c": @{ @".value": @"hello", @".priority": @"c-pri" } + }; + [device do:^(FIRDatabaseReference *ref) { + [self waitForExportValueOf:ref toBe:expected]; + }]; + [device dispose]; +} + +- (void) testServerDataCachedOffline1 { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + __block BOOL done = NO; + id data = @{@"a": @1, @"b": @2}; + [writerRef setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + WAIT_FOR(done); + + // Wait for the data to get it cached. + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:data]; + }]; + + // Should still be there after restart, offline. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:data]; + }]; + + // Children should be there too. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:[ref child:@"a"] toBe:@1]; + }]; + [device dispose]; +} + +- (void) testServerDataCompleteness1 { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + id data = @{@"child": @{@"a": @1, @"b": @2 }, @"other": @"blah"}; + [self waitForCompletionOf:writerRef setValue:data]; + + // Wait for each child to get it cached (but not the parent). + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:[ref child:@"child/a"] toBe:@1]; + [self waitForValueOf:[ref child:@"child/b"] toBe:@2]; + [self waitForValueOf:[ref child:@"other"] toBe:@"blah"]; + }]; + + // Restart, offline, should get child_added events, but not value. + [device restartOffline]; + __block BOOL gotA, gotB; + [device do:^(FIRDatabaseReference *ref) { + FIRDatabaseReference *childRef = [ref child:@"child"]; + [childRef observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot.key isEqualToString:@"a"]) { + XCTAssertEqualObjects(snapshot.value, @1, @"Got a"); + gotA = YES; + } else if ([snapshot.key isEqualToString:@"b"]) { + XCTAssertEqualObjects(snapshot.value, @2, @"Got a"); + gotB = YES; + } else { + XCTFail(@"Unexpected child event."); + } + }]; + + // Listen for value events (which we should *not* get). + [childRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTFail(@"Got a value event with incomplete data!"); + }]; + + // Wait for another location just to make sure we wait long enough that we /would/ get a value event if it + // was coming. + [self waitForValueOf:[ref child:@"other"] toBe:@"blah"]; + }]; + + XCTAssertTrue(gotA && gotB, @"Got a and b."); + [device dispose]; +} + +- (void) testServerDataCompleteness2 { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + id data = @{@"a": @1, @"b": @2}; + [self waitForCompletionOf:writerRef setValue:data]; + + // Wait for the children individually. + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:[ref child:@"a"] toBe:@1]; + [self waitForValueOf:[ref child:@"b"] toBe:@2]; + }]; + + // Should still be there after restart, offline. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // No-op. Just triggering a listen at this location. + }]; + [self waitForValueOf:[ref child:@"a"] toBe:@1]; + [self waitForValueOf:[ref child:@"b"] toBe:@2]; + }]; + [device dispose]; +} + +- (void)testServerDataLimit { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + [self waitForCompletionOf:writerRef setValue:@{@"a": @1, @"b": @2, @"c": @3}]; + + // Cache limit(2) of the data. + [device do:^(FIRDatabaseReference *ref) { + FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2]; + [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }]; + }]; + + // We should be able to get limit(2) data offline, but not the whole node. + [device restartOffline]; + [device do:^(FIRDatabaseReference *ref) { + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTFail(@"Got value event for whole node!"); + }]; + + FIRDatabaseQuery *limitRef = [ref queryLimitedToLast:2]; + [self waitForValueOf:limitRef toBe:@{@"b": @2, @"c": @3 }]; + }]; + [device dispose]; +} + +- (void)testRemoveWhileOfflineAndRestart { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + + [[writerRef child:@"test"] setValue:@"test"]; + [device do:^(FIRDatabaseReference *ref) { + // Cache this location. + __block id val = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + val = snapshot.value; + }]; + [self waitUntil:^BOOL { + return [val isEqual:@{@"test": @"test"}]; + }]; + }]; + [device restartOffline]; + + __block BOOL done = NO; + [writerRef removeValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + WAIT_FOR(done); + + [device goOnline]; + [device waitForIdleUsingWaiter:self]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:[NSNull null]]; + }]; + [device dispose]; +} + + +- (void)testDeltaSyncAfterRestart { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + + [writerRef setValue:@"test"]; + + [device do:^(FIRDatabaseReference *ref) { + // Cache this location. + __block id val = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + val = snapshot.value; + }]; + [self waitUntil:^BOOL { + return [val isEqual:@"test"]; + }]; + XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update."); + }]; + [device restartOnline]; + + [device waitForIdleUsingWaiter:self]; + [device do:^(FIRDatabaseReference *ref) { + [self waitForValueOf:ref toBe:@"test"]; + XCTAssertEqual(ref.repo.dataUpdateCount, 0L, @"Should have gotten no updates."); + }]; + [device dispose]; +} + +- (void)testDeltaSyncWorksWithUnfilteredQuery { + FIRDatabaseReference *writerRef = [FTestHelpers getRandomNode]; + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writerRef description] ]; + + // List must be large enough to trigger delta sync. + NSMutableDictionary *longList = [[NSMutableDictionary alloc] init]; + for(NSInteger i = 0; i < 50; i++) { + NSString *key = [[writerRef childByAutoId] key]; + longList[key] = @{ @"order": @1, @"text": @"This is an awesome message!" }; + } + + [writerRef setValue:longList]; + + [device do:^(FIRDatabaseReference *ref) { + // Cache this location. + [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList]; + XCTAssertEqual(ref.repo.dataUpdateCount, 1L, @"Should have gotten one update."); + }]; + [device restartOffline]; + + // Add a new child while the device is offline. + FIRDatabaseReference *newChildRef = [writerRef childByAutoId]; + NSDictionary *newChild = @{ @"order": @50, @"text": @"This is a new appended child!" }; + + [self waitForCompletionOf:newChildRef setValue:newChild]; + longList[[newChildRef key]] = newChild; + + [device goOnline]; + [device do:^(FIRDatabaseReference *ref) { + // Wait for updated value with new child. + [self waitForValueOf:[ref queryOrderedByChild:@"order"] toBe:longList]; + XCTAssertEqual(ref.repo.rangeMergeUpdateCount, 1L, @"Should have gotten a range merge update."); + }]; + [device dispose]; +} + +- (void) testPutsAreRestoredInOrder { + FDevice *device = [[FDevice alloc] initOffline]; + + // Store puts which should have a putId with 10 which is lexiographical small than 9 + [device do:^(FIRDatabaseReference *ref) { + for (int i = 0; i < 11; i++) { + [ref setValue:[NSNumber numberWithInt:i]]; + } + }]; + + // restart the app offline and observe the data. + [device restartOffline]; + + // Make sure that the write with putId 10 wins, not 9 + id expected = @10; + [device do:^(FIRDatabaseReference *ref) { + [self waitForExportValueOf:ref toBe:expected]; + }]; + [device dispose]; +} + +- (void) testStoreSetsPerf1 { + if (!runPerfTests) return; + // Disable persistence in FDevice for comparison without persistence + FDevice *device = [[FDevice alloc] initOnline]; + + __block BOOL done = NO; + [device do:^(FIRDatabaseReference *ref) { + NSDate *start = [NSDate date]; + [self writeChildren:ref count:1000 size:100 waitForComplete:NO]; + + [self waitForQueue:ref]; + + NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]); + done = YES; + }]; + + WAIT_FOR(done); + [device dispose]; +} + +- (void) testStoreListenPerf1 { + if (!runPerfTests) return; + // Disable persistence in FDevice for comparison without persistence + + // Write 1000 x 100-byte children, to read back. + unsigned int count = 1000; + FIRDatabaseReference *writer = [FTestHelpers getRandomNode]; + [self writeChildren:writer count:count size:100]; + + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]]; + + __block BOOL done = NO; + [device do:^(FIRDatabaseReference *ref) { + NSDate *start = [NSDate date]; + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // Wait to make sure we're done persisting everything. + [self waitForQueue:ref]; + XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data."); + NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]); + done = YES; + }]; + }]; + + WAIT_FOR(done); + [device dispose]; +} + +- (void) testRestoreListenPerf1 { + if (!runPerfTests) return; + + // NOTE: Since this is testing restoration of data from cache after restarting, it only works with persistence on. + + // Write 1000 * 100-byte children, to read back. + unsigned int count = 1000; + FIRDatabaseReference *writer = [FTestHelpers getRandomNode]; + [self writeChildren:writer count:count size:100]; + + FDevice *device = [[FDevice alloc] initOnlineWithUrl:[writer description]]; + + // Get the data cached. + __block BOOL done = NO; + [device do:^(FIRDatabaseReference *ref) { + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data."); + done = YES; + }]; + }]; + WAIT_FOR(done); + + // Restart offline and see how long it takes to restore the data from cache. + [device restartOffline]; + done = NO; + [device do:^(FIRDatabaseReference *ref) { + NSDate *start = [NSDate date]; + [ref observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + // Wait to make sure we're done persisting everything. + XCTAssertEqual(snapshot.childrenCount, count, @"Got correct data."); + [self waitForQueue:ref]; + NSLog(@"Elapsed: %f", [[NSDate date] timeIntervalSinceDate:start]); + done = YES; + }]; + }]; + + WAIT_FOR(done); + [device dispose]; +} + +- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size { + [self writeChildren:writer count:count size:size waitForComplete:YES]; +} + +- (void)writeChildren:(FIRDatabaseReference *)writer count:(unsigned int)count size:(unsigned int)size waitForComplete:(BOOL)waitForComplete { + __block BOOL done = NO; + + NSString *data = [self randomStringOfLength:size]; + for(int i = 0; i < count; i++) { + [[writer childByAutoId] setValue:data withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + if (i == (count - 1)) { + done = YES; + } + }]; + } + if (waitForComplete) { + WAIT_FOR(done); + } +} + +NSString *letters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +- (NSString*) randomStringOfLength:(unsigned int)len { + NSMutableString *randomString = [NSMutableString stringWithCapacity: len]; + + for (int i=0; i<len; i++) { + [randomString appendFormat: @"%C", [letters characterAtIndex: arc4random() % [letters length]]]; + } + return randomString; +} + ++ (NSString *) getFirebaseDir { + NSArray *dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDir = [dirPaths objectAtIndex:0]; + NSString *firebaseDir = [documentsDir stringByAppendingPathComponent:@"firebase"]; + + return firebaseDir; +} + +@end diff --git a/Example/Database/Tests/Integration/FRealtime.h b/Example/Database/Tests/Integration/FRealtime.h new file mode 100644 index 0000000..903ef49 --- /dev/null +++ b/Example/Database/Tests/Integration/FRealtime.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FTestBase.h" + +@interface FRealtime : FTestBase + +@end diff --git a/Example/Database/Tests/Integration/FRealtime.m b/Example/Database/Tests/Integration/FRealtime.m new file mode 100644 index 0000000..e554bfe --- /dev/null +++ b/Example/Database/Tests/Integration/FRealtime.m @@ -0,0 +1,605 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FRealtime.h" +#import "FTupleFirebase.h" +#import "FRepoManager.h" +#import "FUtilities.h" +#import "FParsedUrl.h" +#import "FIRDatabaseConfig_Private.h" + +@implementation FRealtime + +- (void) testUrlParsing { + FParsedUrl* parsed = [FUtilities parseUrl:@"http://www.example.com:9000"]; + XCTAssertTrue([[parsed.path description] isEqualToString:@"/"], @"Got correct path"); + XCTAssertTrue([parsed.repoInfo.host isEqualToString:@"www.example.com:9000"], @"Got correct host"); + XCTAssertTrue([parsed.repoInfo.internalHost isEqualToString:@"www.example.com:9000"], @"Got correct host"); + XCTAssertFalse(parsed.repoInfo.secure, @"Should not be secure, there's a port"); + + parsed = [FUtilities parseUrl:@"http://www.firebaseio.com/foo/bar"]; + XCTAssertTrue([[parsed.path description] isEqualToString:@"/foo/bar"], @"Got correct path"); + XCTAssertTrue([parsed.repoInfo.host isEqualToString:@"www.firebaseio.com"], @"Got correct host"); + XCTAssertTrue([parsed.repoInfo.internalHost isEqualToString:@"www.firebaseio.com"], @"Got correct host"); + XCTAssertTrue(parsed.repoInfo.secure, @"Should be secure, there's no port"); +} + +- (void) testCachingRedirects { + NSString* host = @"host.example.com"; + NSString* host2 = @"host2.example.com"; + NSString* internalHost = @"internal.example.com"; + NSString* internalHost2 = @"internal2.example.com"; + + // Set host on first repo info + FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:host]; + XCTAssertTrue([repoInfo.host isEqualToString:host], @"Got correct host"); + XCTAssertTrue([repoInfo.internalHost isEqualToString:host], @"Got correct host"); + + // Set internal host on first repo info + repoInfo.internalHost = internalHost; + XCTAssertTrue([repoInfo.host isEqualToString:host], @"Got correct host"); + XCTAssertTrue([repoInfo.internalHost isEqualToString:internalHost], @"Got correct host"); + + // Set up a second unrelated repo info to make sure caching is keyspaced properly + FRepoInfo* repoInfo2 = [[FRepoInfo alloc] initWithHost:host2 isSecure:YES withNamespace:host2]; + XCTAssertTrue([repoInfo2.host isEqualToString:host2], @"Got correct host"); + XCTAssertTrue([repoInfo2.internalHost isEqualToString:host2], @"Got correct host"); + + repoInfo2.internalHost = internalHost2; + XCTAssertTrue([repoInfo2.internalHost isEqualToString:internalHost2], @"Got correct host"); + + // Setting host on this repo info should also set the right internal host + FRepoInfo* repoInfoCached = [[FRepoInfo alloc] initWithHost:host isSecure:YES withNamespace:host]; + XCTAssertTrue([repoInfoCached.host isEqualToString:host], @"Got correct host"); + XCTAssertTrue([repoInfoCached.internalHost isEqualToString:internalHost], @"Got correct host"); + + [repoInfo clearInternalHostCache]; + [repoInfo2 clearInternalHostCache]; + [repoInfoCached clearInternalHostCache]; + + XCTAssertTrue([repoInfo.internalHost isEqualToString:host], @"Got correct host"); + XCTAssertTrue([repoInfo2.internalHost isEqualToString:host2], @"Got correct host"); + XCTAssertTrue([repoInfoCached.internalHost isEqualToString:host], @"Got correct host"); +} + +- (void) testOnDisconnectSetWorks { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"]; + + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key]; + + __block NSNumber* readValue = @0; + __block NSNumber* writeValue = @0; + [[reader child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSNumber *val = [snapshot value]; + if (![val isEqual:[NSNull null]]) { + readValue = val; + } + }]; + + [[writer child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + if (val != [NSNull null]) { + writeValue = val; + } + }]; + + [writer child:@"hello"]; + + __block BOOL ready = NO; + [[writer child:@"disconnected"] onDisconnectSetValue:@1 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref){ + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [writer child:@"s"]; + + ready = NO; + [[writer child:@"disconnected"] onDisconnectSetValue:@2 withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref){ + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return [@2 isEqualToNumber:readValue] && [@2 isEqualToNumber:writeValue]; + }]; + + [FRepoManager interrupt:readerCfg]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; + [FRepoManager disposeRepos:readerCfg]; +} + +- (void) testOnDisconnectSetWithPriorityWorks { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"]; + + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key]; + + __block BOOL sawNewValue = NO; + __block BOOL writerSawNewValue = NO; + [[reader child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = snapshot.value; + id pri = snapshot.priority; + if (val != [NSNull null] && pri != [NSNull null]) { + sawNewValue = [(NSNumber *) val boolValue] && [pri isEqualToString:@"abcd"]; + } + }]; + + [[writer child:@"disconnected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + id val = [snapshot value]; + id pri = snapshot.priority; + if (val != [NSNull null] && pri != [NSNull null]) { + writerSawNewValue = [(NSNumber *) val boolValue] && [pri isEqualToString:@"abcd"]; + } + }]; + + __block BOOL ready = NO; + [[writer child:@"disconnected"] onDisconnectSetValue:@YES andPriority:@"abcd" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return sawNewValue && writerSawNewValue; + }]; + + [FRepoManager interrupt:readerCfg]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; + [FRepoManager disposeRepos:readerCfg]; +} + +- (void) testOnDisconnectRemoveWorks { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"]; + + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key]; + + __block BOOL ready = NO; + [[writer child:@"foo"] setValue:@"bar" withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + __block BOOL sawRemove = NO; + __block BOOL writerSawRemove = NO; + [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + sawRemove = [[NSNull null] isEqual:snapshot.value]; + }]; + + [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + writerSawRemove = [[NSNull null] isEqual:snapshot.value]; + }]; + + ready = NO; + [[writer child:@"foo"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return sawRemove && writerSawRemove; + }]; + + [FRepoManager interrupt:readerCfg]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; + [FRepoManager disposeRepos:readerCfg]; +} + +- (void) testOnDisconnectUpdateWorks { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"]; + + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key]; + + [self waitForCompletionOf:[writer child:@"foo"] setValue:@{@"bar": @"a", @"baz": @"b"}]; + + __block BOOL sawNewValue = NO; + __block BOOL writerSawNewValue = NO; + [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *val = [snapshot value]; + if (val) { + sawNewValue = [@{@"bar" : @"a", @"baz" : @"c", @"bat" : @"d"} isEqualToDictionary:val]; + } + }]; + + [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *val = [snapshot value]; + if (val) { + writerSawNewValue = [@{@"bar" : @"a", @"baz" : @"c", @"bat" : @"d"} isEqualToDictionary:val]; + } + }]; + + __block BOOL ready = NO; + [[writer child:@"foo"] onDisconnectUpdateChildValues:@{@"baz": @"c", @"bat": @"d"} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return sawNewValue && writerSawNewValue; + }]; + + [FRepoManager interrupt:readerCfg]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; + [FRepoManager disposeRepos:readerCfg]; +} + +- (void) testOnDisconnectTriggersSingleLocalValueEventForWriter { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + + __block int calls = 0; + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + calls++; + if (calls == 2) { + // second call, verify the data + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"foo" : @{@"bar" : @"a", @"bam" : @"c"}}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one"); + } else if (calls > 2) { + XCTFail(@"Extra calls"); + } + }]; + + [self waitUntil:^BOOL{ + return calls == 1; + }]; + + __block BOOL done = NO; + FIRDatabaseReference * child = [writer child:@"foo"]; + [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}]; + [child onDisconnectUpdateChildValues:@{@"bam": @"c"}]; + [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return calls == 2; + }]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; +} + +- (void) testOnDisconnectTriggersSingleLocalValueEventForReader { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseReference * reader = [FTestHelpers getRandomNode]; + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] child:reader.key]; + + __block int calls = 0; + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + calls++; + if (calls == 2) { + // second call, verify the data + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"foo" : @{@"bar" : @"a", @"bam" : @"c"}}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one"); + } else if (calls > 2) { + XCTFail(@"Extra calls"); + } + }]; + + [self waitUntil:^BOOL{ + return calls == 1; + }]; + + __block BOOL done = NO; + FIRDatabaseReference * child = [writer child:@"foo"]; + [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}]; + [child onDisconnectUpdateChildValues:@{@"bam": @"c"}]; + [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return calls == 2; + }]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; +} + +- (void) testOnDisconnectTriggersSingleLocalValueEventForWriterWithQuery { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + + __block int calls = 0; + [[[writer child:@"foo"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + calls++; + if (calls == 2) { + // second call, verify the data + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"bar" : @"a", @"bam" : @"c"}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one"); + } else if (calls > 2) { + XCTFail(@"Extra calls"); + } + }]; + + [self waitUntil:^BOOL{ + return calls == 1; + }]; + + __block BOOL done = NO; + FIRDatabaseReference * child = [writer child:@"foo"]; + [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}]; + [child onDisconnectUpdateChildValues:@{@"bam": @"c"}]; + [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return calls == 2; + }]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; +} + +- (void) testOnDisconnectTriggersSingleLocalValueEventForReaderWithQuery { + FIRDatabaseReference * reader = [FTestHelpers getRandomNode]; + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] child:reader.key]; + + __block int calls = 0; + [[[reader child:@"foo"] queryLimitedToLast:2] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + calls++; + XCTAssertTrue([snapshot.key isEqualToString:@"foo"], @"Got the right snapshot"); + if (calls == 2) { + // second call, verify the data + NSDictionary *val = [snapshot value]; + NSDictionary *expected = @{@"bar" : @"a", @"bam" : @"c"}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got all of the updates in one"); + } else if (calls > 2) { + XCTFail(@"Extra calls"); + } + }]; + + [self waitUntil:^BOOL{ + return calls == 1; + }]; + + __block BOOL done = NO; + FIRDatabaseReference * child = [writer child:@"foo"]; + [child onDisconnectSetValue:@{@"bar": @"a", @"baz": @"b"}]; + [child onDisconnectUpdateChildValues:@{@"bam": @"c"}]; + [[child child:@"baz"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return calls == 2; + }]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; +} + +- (void) testOnDisconnectDeepMergeTriggersOnlyOneValueEventForReaderWithQuery { + FIRDatabaseReference * reader = [FTestHelpers getRandomNode]; + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + + __block BOOL done = NO; + NSDictionary* toSet = @{@"a": @1, @"b": @{@"c": @YES, @"d": @"scalar", @"e": @{@"f": @"hooray"}}}; + [writer setValue:toSet]; + [[writer child:@"a"] onDisconnectSetValue:@2]; + [[writer child:@"b/d"] onDisconnectRemoveValueWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + WAIT_FOR(done); + + __block int count = 2; + [[reader queryLimitedToLast:3] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + count++; + if (count == 1) { + // Loaded the data, kill the writer connection + [FRepoManager interrupt:writerCfg]; + } else if (count == 2) { + NSDictionary *expected = @{@"a" : @2, @"b" : @{@"c" : @YES, @"e" : @{@"f" : @"hooray"}}}; + XCTAssertTrue([snapshot.value isEqualToDictionary:expected], @"Should see complete new snapshot"); + } else { + XCTFail(@"Too many calls"); + } + }]; + + WAIT_FOR(count == 2); + + // cleanup + [FRepoManager disposeRepos:writerCfg]; +} + + +- (void) testOnDisconnectCancelWorks { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseConfig *readerCfg = [FIRDatabaseConfig configForName:@"reader"]; + + FIRDatabaseReference * writer = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + FIRDatabaseReference * reader = [[[FIRDatabaseReference alloc] initWithConfig:readerCfg] child:writer.key]; + + __block BOOL ready = NO; + [[writer child:@"foo"] setValue:@{@"bar": @"a", @"baz": @"b"} withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + __block BOOL sawNewValue = NO; + __block BOOL writerSawNewValue = NO; + [[reader child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *val = [snapshot value]; + if (val) { + sawNewValue = [@{@"bar" : @"a", @"baz" : @"b", @"bat" : @"d"} isEqualToDictionary:val]; + } + }]; + + [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + NSDictionary *val = [snapshot value]; + if (val) { + writerSawNewValue = [@{@"bar" : @"a", @"baz" : @"b", @"bat" : @"d"} isEqualToDictionary:val]; + } + }]; + + ready = NO; + [[writer child:@"foo"] onDisconnectUpdateChildValues:@{@"baz": @"c", @"bat": @"d"}]; + [[writer child:@"foo/baz"] cancelDisconnectOperationsWithCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + return sawNewValue && writerSawNewValue; + }]; + + [FRepoManager interrupt:readerCfg]; + + // cleanup + [FRepoManager disposeRepos:writerCfg]; + [FRepoManager disposeRepos:readerCfg]; +} + +- (void) testOnDisconnectWithServerValuesWithLocalEvents { + FIRDatabaseConfig *writerCfg = [FIRDatabaseConfig configForName:@"writer"]; + FIRDatabaseReference * node = [[[FIRDatabaseReference alloc] initWithConfig:writerCfg] childByAutoId]; + + __block FIRDataSnapshot *snap = nil; + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + NSDictionary* data = @{ + @"a": @1, + @"b": @{ + @".value": [FIRServerValue timestamp], + @".priority": [FIRServerValue timestamp] + } + }; + + __block BOOL done = NO; + [node onDisconnectSetValue:data andPriority:[FIRServerValue timestamp] withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + [node onDisconnectUpdateChildValues:@{ @"a": [FIRServerValue timestamp], @"c": [FIRServerValue timestamp] } withCompletionBlock:^(NSError* error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + [FRepoManager interrupt:writerCfg]; + + [self waitUntil:^BOOL{ + if ([snap value] != [NSNull null]) { + NSDictionary* val = [snap value]; + done = (val[@"a"] && val[@"b"] && val[@"c"]); + } + return done; + }]; + + NSDictionary* value = [snap value]; + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + NSNumber* timestamp = [snap priority]; + XCTAssertTrue([[snap priority] isKindOfClass:[NSNumber class]], @"Should get back number"); + XCTAssertTrue([now doubleValue] - [timestamp doubleValue] < 2000, @"Number should be no more than 2 seconds ago"); + XCTAssertEqualObjects([snap priority], [value objectForKey:@"a"], @"Should get back matching ServerValue.TIMESTAMP"); + XCTAssertEqualObjects([snap priority], [value objectForKey:@"b"], @"Should get back matching ServerValue.TIMESTAMP"); + XCTAssertEqualObjects([snap priority], [[snap childSnapshotForPath:@"b"] priority], @"Should get back matching ServerValue.TIMESTAMP"); + XCTAssertEqualObjects([NSNull null], [[snap childSnapshotForPath:@"d"] value], @"Should get null for cancelled child"); + + // cleanup + [FRepoManager disposeRepos:writerCfg]; +} + +@end diff --git a/Example/Database/Tests/Integration/FTransactionTest.h b/Example/Database/Tests/Integration/FTransactionTest.h new file mode 100644 index 0000000..6bb7d4d --- /dev/null +++ b/Example/Database/Tests/Integration/FTransactionTest.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestBase.h" + +@interface FTransactionTest : FTestBase + +@end diff --git a/Example/Database/Tests/Integration/FTransactionTest.m b/Example/Database/Tests/Integration/FTransactionTest.m new file mode 100644 index 0000000..b78615b --- /dev/null +++ b/Example/Database/Tests/Integration/FTransactionTest.m @@ -0,0 +1,1382 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTransactionTest.h" +#import "FTestHelpers.h" +#import "FEventTester.h" +#import "FTupleEventTypeString.h" +#import "FIRDatabaseQuery_Private.h" +#import "FIRDatabaseConfig_Private.h" + + +// HACK used by testUnsentTransactionsAreNotCancelledOnDisconnect to return one bad token and then a nil token. +@interface FIROneBadTokenProvider : NSObject <FAuthTokenProvider> { + BOOL firstFetch; +} +@end + +@implementation FIROneBadTokenProvider +- (instancetype) init { + self = [super init]; + if (self) { + firstFetch = YES; + } + return self; +} + +- (void) fetchTokenForcingRefresh:(BOOL)forceRefresh withCallback:(fbt_void_nsstring_nserror)callback { + // Simulate delay + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), [FIRDatabaseQuery sharedQueue], ^{ + if (firstFetch) { + firstFetch = NO; + callback(@"bad-token", nil); + } else { + callback(nil, nil); + } + }); +} + +- (void) listenForTokenChanges:(fbt_void_nsstring)listener { +} + +@end +@implementation FTransactionTest + +- (void) testNewValueIsImmediatelyVisible { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL runOnce = NO; + [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){ + runOnce = YES; + [currentValue setValue:@42]; + return [FIRTransactionResult successWithValue:currentValue]; + }]; + + [self waitUntil:^BOOL{ + return runOnce; + }]; + + __block BOOL ready = NO; + [[node child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (!ready) { + NSNumber *val = [snapshot value]; + XCTAssertTrue([val isEqualToNumber:@42], @"Got value set in transaction"); + ready = YES; + } + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testNonAbortedTransactionSetsCommittedToTrueInCallback { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){ + [currentValue setValue:@42]; + return [FIRTransactionResult successWithValue:currentValue]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should not have aborted"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testAbortedTransactionSetsCommittedToFalseInCallback { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + [[node child:@"foo"] runTransactionBlock:^(FIRMutableData * currentValue){ + return [FIRTransactionResult abort]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertFalse(committed, @"Should have aborted"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testBugTestSetDataReconnectDoTransactionThatAbortsOnceDataArrivesVerifyCorrectEvents { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * reader = refs.one; + + __block BOOL dataWritten = NO; + [[reader child:@"foo"] setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + dataWritten = YES; + }]; + + [self waitUntil:^BOOL{ + return dataWritten; + }]; + + FIRDatabaseReference * writer = refs.two; + __block int eventsReceived = 0; + [[writer child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if (eventsReceived == 0) { + NSString *val = [snapshot value]; + XCTAssertTrue([val isEqualToString:@"temp value"], @"Got initial transaction value"); + } else if (eventsReceived == 1) { + NSNumber *val = [snapshot value]; + XCTAssertTrue([val isEqualToNumber:@42], @"Got hidden original value"); + } else { + XCTFail(@"Too many events"); + } + eventsReceived++; + }]; + + [[writer child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id current = [currentData value]; + if (current == [NSNull null]) { + [currentData setValue:@"temp value"]; + return [FIRTransactionResult successWithValue:currentData]; + } else { + return [FIRTransactionResult abort]; + } + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertFalse(committed, @"This transaction should never commit"); + XCTAssertTrue(error == nil, @"This transaction should not have an error"); + }]; + + [self waitUntil:^BOOL{ + return eventsReceived == 2; + }]; + +} + +- (void) testUseTransactionToCreateANodeMakeSureExactlyOneEventIsReceived { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block int events = 0; + __block BOOL done = NO; + + [[node child:@"a"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + events++; + if (events > 1) { + XCTFail(@"Too many events"); + } + }]; + + [[node child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@42]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done && events == 1; + }]; +} + +- (void) testUseTransactionToUpdateTwoExistingChildNodesMakeSureEventsAreOnlyRaisedForChangedNode { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * node1 = [refs.one child:@"foo"]; + FIRDatabaseReference * node2 = [refs.two child:@"foo"]; + + __block BOOL ready = NO; + [[node1 child:@"a"] setValue:@42]; + [[node1 child:@"b"] setValue:@42 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + FEventTester* et = [[FEventTester alloc] initFrom:self]; + NSArray* expect = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"a"] withEvent:FIRDataEventTypeValue withString:nil], + [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:expect]; + [et wait]; + + expect = @[ + [[FTupleEventTypeString alloc] initWithFirebase:[node2 child:@"b"] withEvent:FIRDataEventTypeValue withString:nil] + ]; + + [et addLookingFor:expect]; + + ready = NO; + [node2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + NSDictionary* toSet = @{@"a": @42, @"b": @87}; + [currentData setValue:toSet]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + [et wait]; +} + +- (void) testTransactionOnlyCalledOnceWhenInitializingAnEmptyNode { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block BOOL updateCalled = NO; + [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + XCTAssertTrue(val == [NSNull null], @"Should be no value here to start with"); + if (updateCalled) { + XCTFail(@"Should not be called again"); + } + updateCalled = YES; + [currentData setValue:@{@"a": @5, @"b": @6}]; + return [FIRTransactionResult successWithValue:currentData]; + }]; + + [self waitUntil:^BOOL{ + return updateCalled; + }]; +} + +- (void) testSecondTransactionGetsRunImmediatelyOnPreviousOutputAndOnlyRunsOnce { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * ref1 = refs.one; + FIRDatabaseReference * ref2 = refs.two; + + __block BOOL firstRun = NO; + __block BOOL firstDone = NO; + __block BOOL secondRun = NO; + __block BOOL secondDone = NO; + + [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + XCTAssertFalse(firstRun, @"Should not be run twice"); + firstRun = YES; + [currentData setValue:@42]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should not fail"); + firstDone = YES; + }]; + + [self waitUntil:^BOOL{ + return firstRun; + }]; + + [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + XCTAssertFalse(secondRun, @"Should only run once"); + secondRun = YES; + NSNumber* val = [currentData value]; + XCTAssertTrue([val isEqualToNumber:@42], @"Should see result of last transaction"); + [currentData setValue:@84]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should not fail"); + secondDone = YES; + }]; + + [self waitUntil:^BOOL{ + return secondRun; + }]; + + __block FIRDataSnapshot * snap = nil; + [ref1 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap value] isEqualToNumber:@84], @"Should get updated value"); + + [self waitUntil:^BOOL{ + return firstDone && secondDone; + }]; + + snap = nil; + [ref2 observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [self waitUntil:^BOOL{ + return snap != nil; + }]; + + XCTAssertTrue([[snap value] isEqualToNumber:@84], @"Should get updated value"); +} + +// The js test, "Set() cancels pending transactions and re-runs affected transactions.", does not cleanly port to ios +// due to everything being asynchronous. Rather than attempt to mitigate the various race conditions inherent in a port, +// I'm adding tests to cover the specific behaviors wrapped up in that one test. + +- (void) testSetCancelsPendingTransaction { + FIRDatabaseReference * node = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot * nodeSnap = nil; + __block FIRDataSnapshot * nodeFooSnap = nil; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nodeSnap = snapshot; + }]; + + [[node child:@"foo"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nodeFooSnap = snapshot; + }]; + + __block BOOL firstDone = NO; + __block BOOL secondDone = NO; + __block BOOL firstRun = NO; + + [[node child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + XCTAssertFalse(firstRun, @"Should only run once"); + firstRun = YES; + [currentData setValue:@42]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should not fail"); + firstDone = YES; + }]; + + [self waitUntil:^BOOL{ + return nodeFooSnap != nil; + }]; + + XCTAssertTrue([[nodeFooSnap value] isEqualToNumber:@42], @"Got first value"); + + [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@{@"foo": @84, @"bar": @1}]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertFalse(committed, @"This should not ever be committed"); + secondDone = YES; + }]; + + [self waitUntil:^BOOL{ + return nodeSnap != nil; + }]; + + [[node child:@"foo"] setValue:@0]; +} + +// It's difficult to force a transaction re-run on ios, since everything is async. There is also an outstanding case that prevents +// this test from being before a connection is established (#1981) +/* +- (void) testSetRerunsAffectedTransactions { + + Firebase* node = [FTestHelpers getRandomNode]; + + __block BOOL ready = NO; + [[node.parent child:@".info/connected"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + ready = [[snapshot value] boolValue]; + }]; + [self waitUntil:^BOOL{ + return ready; + }]; + + __block FIRDataSnapshot* nodeSnap = nil; + + [node observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + nodeSnap = snapshot; + NSLog(@"SNAP value: %@", [snapshot value]); + }]; + + __block BOOL firstDone = NO; + __block BOOL secondDone = NO; + __block BOOL firstRun = NO; + __block int secondCount = 0; + __block BOOL setDone = NO; + + [node runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + STAssertFalse(firstRun, @"Should only run once"); + firstRun = YES; + [currentData setValue:@42]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + STAssertTrue(committed, @"Should not fail"); + firstDone = YES; + }]; + + [[node child:@"bar"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + NSLog(@"RUNNING TRANSACTION"); + secondCount++; + id val = [currentData value]; + if (secondCount == 1) { + STAssertTrue(val == [NSNull null], @"Should not have a value"); + [currentData setValue:@"first"]; + return [FIRTransactionResult successWithValue:currentData]; + } else if (secondCount == 2) { + NSLog(@"val: %@", val); + STAssertTrue(val == [NSNull null], @"Should not have a value"); + [currentData setValue:@"second"]; + return [FIRTransactionResult successWithValue:currentData]; + } else { + STFail(@"Called too many times"); + return [FIRTransactionResult abort]; + } + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + STAssertTrue(committed, @"Should eventually be committed"); + secondDone = YES; + }]; + + [[node child:@"foo"] setValue:@0 andCompletionBlock:^(NSError *error) { + setDone = YES; + }]; + + [self waitUntil:^BOOL{ + return setDone; + }]; + + NSDictionary* expected = @{@"bar": @"second", @"foo": @0}; + STAssertTrue([[nodeSnap value] isEqualToDictionary:expected], @"Got last value"); + + STAssertTrue(secondCount == 2, @"Should have re-run second transaction"); +}*/ + +- (void) testTransactionSetSetWorks { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + XCTAssertTrue(val == [NSNull null], @"Initial data should be null"); + [currentData setValue:@"hi!"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error == nil, @"Should not be an error"); + XCTAssertTrue(committed, @"Should commit"); + done = YES; + }]; + + [ref setValue:@"foo"]; + [ref setValue:@"bar"]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testPriorityIsNotPreservedWhenSettingData { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block FIRDataSnapshot * snap = nil; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + snap = snapshot; + }]; + + [ref setValue:@"test" andPriority:@5]; + + __block BOOL ready = NO; + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"new value"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + id val = [snap value]; + id pri = [snap priority]; + XCTAssertTrue(pri == [NSNull null], @"Got priority"); + XCTAssertTrue([val isEqualToString:@"new value"], @"Get new value"); +} + +// Skipping test with nested transactions. Everything is async on ios, so new transactions just get placed in a queue + +- (void) testResultSnapshotIsPassedToOnComplete { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * ref1 = refs.one; + FIRDatabaseReference * ref2 = refs.two; + + __block BOOL done = NO; + [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + if (val == [NSNull null]) { + [currentData setValue:@"hello!"]; + return [FIRTransactionResult successWithValue:currentData]; + } else { + return [FIRTransactionResult abort]; + } + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should commit"); + XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + // do it again for the aborted case + + done = NO; + [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + if (val == [NSNull null]) { + [currentData setValue:@"hello!"]; + return [FIRTransactionResult successWithValue:currentData]; + } else { + return [FIRTransactionResult abort]; + } + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertFalse(committed, @"Should not commit"); + XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + // do it again on a fresh connection, for the aborted case + done = NO; + [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + if (val == [NSNull null]) { + [currentData setValue:@"hello!"]; + return [FIRTransactionResult successWithValue:currentData]; + } else { + return [FIRTransactionResult abort]; + } + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertFalse(committed, @"Should not commit"); + XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"Got correct snapshot"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testTransactionAbortsAfter25Retries { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + [ref.repo setHijackHash:YES]; + + __block int tries = 0; + __block BOOL done = NO; + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + XCTAssertTrue(tries < 25, @"Should not be more than 25 tries"); + tries++; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error != nil, @"Should fail, too many retries"); + XCTAssertFalse(committed, @"Should not commit"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + [ref.repo setHijackHash:NO]; +} + +- (void) testSetShouldCancelSentTransactionsThatComeBackAsDatastale { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * ref1 = refs.one; + FIRDatabaseReference * ref2 = refs.two; + + __block BOOL ready = NO; + [ref1 setValue:@5 withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + XCTAssertTrue(val == [NSNull null], @"No current value"); + [currentData setValue:@72]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error != nil, @"Should abort"); + XCTAssertFalse(committed, @"Should not commit"); + ready = YES; + }]; + + [ref2 setValue:@32]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testUpdateShouldNotCancelUnrelatedTransactions { + FIRDatabaseReference* ref = [FTestHelpers getRandomNode]; + + __block BOOL fooTransactionDone = NO; + __block BOOL barTransactionDone = NO; + + [self waitForCompletionOf:[ref child:@"foo"] setValue:@"oldValue"]; + + [ref.repo setHijackHash:YES]; + + // This transaction should get cancelled as we update "foo" later on. + [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@72]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error != nil, @"Should abort"); + XCTAssertFalse(committed, @"Should not commit"); + fooTransactionDone = YES; + }]; + + // This transaction should not get cancelled since we don't update "bar". + [[ref child:@"bar"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@72]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + // Note: In rare cases, this might get aborted since failed transactions (forced by setHijackHash) are only + // retried 25 times. If we hit this limit before we stop hijacking the hash below, this test will flake. + XCTAssertTrue(error == nil, @"Should not abort"); + XCTAssertTrue(committed, @"Should commit"); + barTransactionDone = YES; + }]; + + NSDictionary *udpateData = @{ @"foo": @"newValue", + @"boo": @"newValue", + @"doo/foo": @"newValue", + @"loo" : @{ @"doo": @{ @"boo":@"newValue"}}} ; + + [self waitForCompletionOf:ref updateChildValues:udpateData]; + XCTAssertTrue(fooTransactionDone, "Should have gotten cancelled before the update"); + XCTAssertFalse(barTransactionDone, "Should run after the update"); + [ref.repo setHijackHash:NO]; + + WAIT_FOR(barTransactionDone); +} + +- (void) testTransactionOnWackyUnicode { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * ref1 = refs.one; + FIRDatabaseReference * ref2 = refs.two; + + __block BOOL ready = NO; + [ref1 setValue:@"♜♞♝♛♚♝♞♜" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; + + ready = NO; + [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + if (val != [NSNull null]) { + XCTAssertTrue([val isEqualToString:@"♜♞♝♛♚♝♞♜"], @"Got crazy unicode"); + } + [currentData setValue:@"♖♘♗♕♔♗♘♖"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error == nil, @"Should not abort"); + XCTAssertTrue(committed, @"Should commit"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testImmediatelyAbortedTransactions { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult abort]; + }]; + + __block BOOL ready = NO; + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult abort]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error == nil, @"No error occurred, we just aborted"); + XCTAssertFalse(committed, @"Should not commit"); + ready = YES; + }]; + + [self waitUntil:^BOOL{ + return ready; + }]; +} + +- (void) testAddingToAnArrayWithATransaction { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + + __block BOOL done = NO; + [ref setValue:@[@"cat", @"horse"] withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + if (val != [NSNull null]) { + NSArray* arr = val; + NSMutableArray* toSet = [arr mutableCopy]; + [toSet addObject:@"dog"]; + [currentData setValue:toSet]; + return [FIRTransactionResult successWithValue:currentData]; + } else { + [currentData setValue:@[@"dog"]]; + return [FIRTransactionResult successWithValue:currentData]; + } + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should commit"); + NSArray* val = [snapshot value]; + NSArray* expected = @[@"cat", @"horse", @"dog"]; + XCTAssertTrue([val isEqualToArray:expected], @"Got whole array"); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testMergedTransactionsHaveCorrectSnapshotInOnComplete { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * node1 = refs.one; + FIRDatabaseReference * node2 = refs.two; + + __block BOOL done = NO; + [node1 setValue:@{@"a": @0} withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + __block BOOL transaction1Done = NO; + __block BOOL transaction2Done = NO; + + [node2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + if (val != [NSNull null]) { + XCTAssertTrue([@{@"a": @0} isEqualToDictionary:val], @"Got initial data"); + } + [currentData setValue:@{@"a": @1}]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should commit"); + XCTAssertTrue([snapshot.key isEqualToString:node2.key], @"Correct snapshot name"); + NSDictionary* val = [snapshot value]; + // Per new behavior, will include the accepted value of the transaction, if it was successful. + NSDictionary* expected = @{@"a": @1}; + XCTAssertTrue([val isEqualToDictionary:expected], @"Got final result"); + transaction1Done = YES; + }]; + + [[node2 child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id val = [currentData value]; + if (val != [NSNull null]) { + XCTAssertTrue([@1 isEqualToNumber:val], @"Got initial data"); + } + [currentData setValue:@2]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(committed, @"Should commit"); + XCTAssertTrue([snapshot.key isEqualToString:@"a"], @"Correct snapshot name"); + NSNumber* val = [snapshot value]; + NSNumber* expected = @2; + XCTAssertTrue([val isEqualToNumber:expected], @"Got final result"); + transaction2Done = YES; + }]; + + [self waitUntil:^BOOL{ + return transaction1Done && transaction2Done; + }]; +} + +// Skipping two tests on nested calls. Since iOS uses a work queue, nested calls don't actually happen synchronously, so they aren't problematic + +- (void) testPendingTransactionsAreCancelledOnDisconnect { + FIRDatabaseConfig *cfg = [FIRDatabaseConfig configForName:@"pending-transactions"]; + FIRDatabaseReference * ref = [[[FIRDatabaseReference alloc] initWithConfig:cfg] childByAutoId]; + + __block BOOL done = NO; + [[ref child:@"a"] setValue:@"initial" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; + + done = NO; + [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"new"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertFalse(committed, @"Should not commit"); + XCTAssertTrue(error != nil, @"Should be an error"); + done = YES; + }]; + + [FRepoManager interrupt:cfg]; + + [self waitUntil:^BOOL{ + return done; + }]; + + // cleanup + [FRepoManager interrupt:cfg]; + [FRepoManager disposeRepos:cfg]; +} + +- (void) testTransactionWithoutLocalEvents1 { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + NSMutableArray* values = [[NSMutableArray alloc] init]; + [ref observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [values addObject:[snapshot value]]; + }]; + + [self waitUntil:^BOOL{ + // get initial data + return values.count > 0; + }]; + + __block BOOL done = NO; + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"hello!"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error == nil, @"Should not be an error"); + XCTAssertTrue(committed, @"Committed"); + XCTAssertTrue([[snapshot value] isEqualToString:@"hello!"], @"got correct snapshot"); + done = YES; + } withLocalEvents:NO]; + + NSArray* expected = @[[NSNull null]]; + XCTAssertTrue([values isEqualToArray:expected], @"Should not have gotten any values yet"); + + [self waitUntil:^BOOL{ + return done; + }]; + + expected = @[[NSNull null], @"hello!"]; + XCTAssertTrue([values isEqualToArray:expected], @"Should have the new value now"); +} + +- (void) testTransactionWithoutLocalEvents2 { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * ref1 = refs.one; + FIRDatabaseReference * ref2 = refs.two; + int SETS = 4; + + [ref1.repo setHijackHash:YES]; + + NSMutableArray* events = [[NSMutableArray alloc] init]; + [ref1 setValue:@0]; + [ref1 observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [events addObject:[snapshot value]]; + }]; + + [self waitUntil:^BOOL{ + return events.count > 0; + }]; + + NSArray* expected = @[@0]; + XCTAssertTrue([events isEqualToArray:expected], @"Got initial set"); + + __block int retries = 0; + __block BOOL done = NO; + [ref1 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + retries++; + id val = [currentData value]; + NSNumber* num = @0; + if (val != [NSNull null]) { + num = val; + } + int eventCount = [num intValue]; + if (eventCount == SETS - 1) { + [ref1.repo setHijackHash:NO]; + } + + [currentData setValue:@"txn result"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertTrue(error == nil, @"Should not be an error"); + XCTAssertTrue(committed, @"Committed"); + XCTAssertTrue([[snapshot value] isEqualToString:@"txn result"], @"got correct snapshot"); + done = YES; + } withLocalEvents:NO]; + + // Meanwhile, do sets from the second connection + for (int i = 0; i < SETS; ++i) { + __block BOOL setDone = NO; + [ref2 setValue:[NSNumber numberWithInt:i] withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + setDone = YES; + }]; + [self waitUntil:^BOOL{ + return setDone; + }]; + } + + [self waitUntil:^BOOL{ + return done; + }]; + + XCTAssertTrue(retries > 0, @"Transaction should have retried"); + XCTAssertEqualObjects([events lastObject], @"txn result", @"Final value matches expected value from txn"); +} + +// Skipping test of calling transaction from value callback. Since all api calls are async on iOS, nested calls are not a problem. + +- (void) testTransactionRevertsDataWhenAddADeeperListen { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * ref1 = refs.one; + FIRDatabaseReference * ref2 = refs.two; + + __block BOOL done = NO; + [[ref1 child:@"y"] setValue:@"test" withCompletionBlock:^(NSError *error, FIRDatabaseReference * ref) { + [ref2 runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + if (currentData.value == [NSNull null]) { + [[currentData childDataByAppendingPath:@"x"] setValue:@5]; + return [FIRTransactionResult successWithValue:currentData]; + } else { + return [FIRTransactionResult abort]; + } + }]; + + [[ref2 child:@"y"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot.value isEqual:@"test"]) { + done = YES; + } + }]; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testTransactionWithIntegerKeys { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block BOOL done = NO; + NSDictionary* toSet = @{@"1": @1, @"5": @5, @"10": @10, @"20": @20}; + [ref setValue:toSet withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@42]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @"Error should be nil."); + XCTAssertTrue(committed, @"Transaction should have committed."); + done = YES; + }]; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +// https://app.asana.com/0/5673976843758/9259161251948 +- (void) testBubbleAppTransactionBug { + FIRDatabaseReference * ref = [FTestHelpers getRandomNode]; + __block BOOL done = NO; + [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@1]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }]; + + [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + NSNumber* val = currentData.value; + NSNumber *new = [NSNumber numberWithInt:(val.intValue + 42)]; + [currentData setValue:new]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }]; + + [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@7]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { }]; + + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + NSNumber* a = [currentData childDataByAppendingPath:@"a"].value; + NSNumber* b = [currentData childDataByAppendingPath:@"b"].value; + NSNumber *new = [NSNumber numberWithInt:a.intValue + b.intValue]; + [currentData setValue:new]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @"Error should be nil."); + XCTAssertTrue(committed, @"Committed should be true."); + XCTAssertEqualObjects(@50, snapshot.value, @"Result should be 50."); + done = YES; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +// If we have cached data, transactions shouldn't run on null. +- (void) testTransactionsAreRunInitiallyOnCurrentlyCachedData { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + id initialData = @{ + @"a": @"a-val", + @"b": @"b-val" + }; + __block BOOL done = NO; + __weak FIRDatabaseReference *weakRef = ref; + [ref setValue:initialData withCompletionBlock:^(NSError *error, FIRDatabaseReference *r) { + [weakRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + [weakRef runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + XCTAssertEqualObjects(currentData.value, initialData, @"Should be initial data."); + done = YES; + return [FIRTransactionResult abort]; + }]; + }]; + }]; + + [self waitUntil:^BOOL{ + return done; + }]; +} + +- (void) testMultipleLevels { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + __block BOOL done = NO; + + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult successWithValue:currentData]; + }]; + + [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult successWithValue:currentData]; + }]; + + [[ref child:@"b"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult successWithValue:currentData]; + }]; + + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + done = YES; + }]; + + WAIT_FOR(done); +} + +- (void) testLocalServerValuesEventuallyButNotImmediatelyMatchServerWithTxns { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * writer = refs.one; + FIRDatabaseReference * reader = refs.two; + __block int done = 0; + + NSMutableArray* readSnaps = [[NSMutableArray alloc] init]; + NSMutableArray* writeSnaps = [[NSMutableArray alloc] init]; + + [reader observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot value] != [NSNull null]) { + [readSnaps addObject:snapshot]; + if (readSnaps.count == 1) { + done += 1; + } + } + }]; + + [writer observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + if ([snapshot value] != [NSNull null]) { + [writeSnaps addObject:snapshot]; + if (writeSnaps.count == 2) { + done += 1; + } + } + }]; + + [writer runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:[FIRServerValue timestamp]]; + [currentData setPriority:[FIRServerValue timestamp]]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) {}]; + + [self waitUntil:^BOOL{ + return done == 2; + }]; + + XCTAssertEqual((unsigned long)[readSnaps count], (unsigned long)1, @"Should have received one snapshot on reader"); + XCTAssertEqual((unsigned long)[writeSnaps count], (unsigned long)2, @"Should have received two snapshots on writer"); + + FIRDataSnapshot * firstReadSnap = [readSnaps objectAtIndex:0]; + FIRDataSnapshot * firstWriteSnap = [writeSnaps objectAtIndex:0]; + FIRDataSnapshot * secondWriteSnap = [writeSnaps objectAtIndex:1]; + + NSNumber* now = [NSNumber numberWithDouble:round([[NSDate date] timeIntervalSince1970]*1000)]; + XCTAssertTrue([now doubleValue] - [firstWriteSnap.value doubleValue] < 2000, @"Should have received a local event with a value close to timestamp"); + XCTAssertTrue([now doubleValue] - [firstWriteSnap.priority doubleValue] < 2000, @"Should have received a local event with a priority close to timestamp"); + XCTAssertTrue([now doubleValue] - [secondWriteSnap.value doubleValue] < 2000, @"Should have received a server event with a value close to timestamp"); + XCTAssertTrue([now doubleValue] - [secondWriteSnap.priority doubleValue] < 2000, @"Should have received a server event with a priority close to timestamp"); + + XCTAssertFalse([firstWriteSnap value] == [secondWriteSnap value], @"Initial and future writer values should be different"); + XCTAssertFalse([firstWriteSnap priority] == [secondWriteSnap priority], @"Initial and future writer priorities should be different"); + XCTAssertEqualObjects(firstReadSnap.value, secondWriteSnap.value, @"Eventual reader and writer values should be equal"); + XCTAssertEqualObjects(firstReadSnap.priority, secondWriteSnap.priority, @"Eventual reader and writer priorities should be equal"); +} + +- (void) testTransactionWithQueryListen { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + __block BOOL done = NO; + + [ref setValue:@{@"a": @1, @"b": @2} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + [[ref queryLimitedToFirst:1] observeEventType:FIRDataEventTypeChildAdded andPreviousSiblingKeyWithBlock:^(FIRDataSnapshot *snapshot, NSString *prevName) { + } withCancelBlock:^(NSError *error) { + }]; + + [[ref child:@"a"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @"This transaction should not have an error"); + XCTAssertTrue(committed, @"Should not have aborted"); + XCTAssertEqualObjects([snapshot value], @1, @"Transaction value should match initial set"); + done = YES; + }]; + }]; + + WAIT_FOR(done); +} + +- (void) testTransactionDoesNotPickUpCachedDataFromPreviousOnce { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * me = refs.one; + FIRDatabaseReference * other = refs.two; + __block BOOL done = NO; + + [me setValue:@"not null" withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + + WAIT_FOR(done); + done = NO; + + [me observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { + done = YES; + }]; + + WAIT_FOR(done); + done = NO; + + [other setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + + WAIT_FOR(done); + done = NO; + + [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id current = [currentData value]; + if (current == [NSNull null]) { + [currentData setValue:@"it was null!"]; + } else { + [currentData setValue:@"it was not null!"]; + } + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @"This transaction should not have an error"); + XCTAssertTrue(committed, @"Should not have aborted"); + XCTAssertEqualObjects([snapshot value], @"it was null!", @"Transaction value should match remote null set"); + done = YES; + }]; + + WAIT_FOR(done); +} + +- (void) testTransactionDoesNotPickUpCachedDataFromPreviousTransaction { + FTupleFirebase* refs = [FTestHelpers getRandomNodePair]; + FIRDatabaseReference * me = refs.one; + FIRDatabaseReference * other = refs.two; + __block BOOL done = NO; + + [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"not null"]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @"This transaction should not have an error"); + XCTAssertTrue(committed, @"Should not have aborted"); + done = YES; + }]; + + WAIT_FOR(done); + done = NO; + + [other setValue:[NSNull null] withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + done = YES; + }]; + + WAIT_FOR(done); + done = NO; + + [me runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id current = [currentData value]; + if (current == [NSNull null]) { + [currentData setValue:@"it was null!"]; + } else { + [currentData setValue:@"it was not null!"]; + } + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @"This transaction should not have an error"); + XCTAssertTrue(committed, @"Should not have aborted"); + XCTAssertEqualObjects([snapshot value], @"it was null!", @"Transaction value should match remote null set"); + done = YES; + }]; + + WAIT_FOR(done); +} + +- (void) testTransactionOnQueriedLocationDoesntRunInitiallyOnNull { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + __block BOOL txnDone = NO; + + [self waitForCompletionOf:[ref childByAutoId] setValue:@{ @"a": @1, @"b": @2 }]; + + [[ref queryLimitedToFirst:1] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [snapshot.ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + id expected = @{@"a" : @1, @"b" : @2}; + XCTAssertEqualObjects(currentData.value, expected, @""); + [currentData setValue:[NSNull null]]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @""); + XCTAssertTrue(committed, @""); + XCTAssertEqualObjects(snapshot.value, [NSNull null], @""); + txnDone = YES; + }]; + }]; + + WAIT_FOR(txnDone); +} + +- (void) testTransactionsRaiseCorrectChildChangedEventsOnQueries { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + __block BOOL txnDone = NO; + NSMutableArray *snapshots = [[NSMutableArray alloc] init]; + + [self waitForCompletionOf:ref setValue:@{ @"foo": @{ @"value": @1 }}]; + + FIRDatabaseQuery *query = [ref queryEndingAtValue:@(DBL_MIN)]; + + [query observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { + [snapshots addObject:snapshot]; + }]; + + [query observeEventType:FIRDataEventTypeChildChanged withBlock:^(FIRDataSnapshot *snapshot) { + [snapshots addObject:snapshot]; + }]; + + [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [[currentData childDataByAppendingPath:@"value"] setValue:@2]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @""); + XCTAssertTrue(committed, @""); + txnDone = YES; + } withLocalEvents:NO]; + + WAIT_FOR(txnDone); + + XCTAssertTrue(snapshots.count == 2, @""); + FIRDataSnapshot *addedSnapshot = snapshots[0]; + XCTAssertEqualObjects(addedSnapshot.key, @"foo", @""); + XCTAssertEqualObjects(addedSnapshot.value, @{ @"value": @1 }, @""); + + FIRDataSnapshot *changedSnapshot = snapshots[1]; + XCTAssertEqualObjects(changedSnapshot.key, @"foo", @""); + XCTAssertEqualObjects(changedSnapshot.value, @{ @"value": @2 }, @""); +} + +- (void) testTransactionsUseLocalMerges { + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + __block BOOL txnDone = NO; + [ref updateChildValues:@{ @"foo": @"bar"}]; + + [[ref child:@"foo"] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + XCTAssertEqualObjects(currentData.value, @"bar", @"Transaction value matches local updates"); + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error, @""); + XCTAssertTrue(committed, @""); + txnDone = YES; + }]; + + WAIT_FOR(txnDone); +} + +//See https://app.asana.com/0/15566422264127/23303789496881 +- (void)testOutOfOrderRemoveWritesAreHandledCorrectly +{ + FIRDatabaseReference *ref = [FTestHelpers getRandomNode]; + [ref setValue:@{@"foo": @"bar"}]; + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"transaction-1"]; + return [FIRTransactionResult successWithValue:currentData]; + }]; + [ref runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@"transaction-2"]; + return [FIRTransactionResult successWithValue:currentData]; + }]; + __block BOOL done = NO; + // This will trigger an abort of the transaction which should not cause the client to crash + [ref updateChildValues:@{@"qux": @"quu"} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { + XCTAssertNil(error); + done = YES; + }]; + + WAIT_FOR(done); +} + +- (void)testUnsentTransactionsAreNotCancelledOnDisconnect { + // Hack: To trigger us to disconnect before restoring state, we inject a bad auth token. + // In real-world usage the much more common case is that we get redirected to a different + // server, but that's harder to manufacture from a test. + NSString *configName = @"testUnsentTransactionsAreNotCancelledOnDisconnect"; + FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:configName]; + config.authTokenProvider = [[FIROneBadTokenProvider alloc] init]; + + // Queue a transaction offline. + FIRDatabaseReference *root = [[FIRDatabaseReference alloc] initWithConfig:config]; + [root.database goOffline]; + __block BOOL done = NO; + [[root childByAutoId] runTransactionBlock:^FIRTransactionResult *(FIRMutableData *currentData) { + [currentData setValue:@0]; + return [FIRTransactionResult successWithValue:currentData]; + } andCompletionBlock:^(NSError *error, BOOL committed, FIRDataSnapshot *snapshot) { + XCTAssertNil(error); + XCTAssertTrue(committed); + done = YES; + }]; + + [root.database goOnline]; + WAIT_FOR(done); +} + +@end diff --git a/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m b/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m new file mode 100644 index 0000000..cdc9e1c --- /dev/null +++ b/Example/Database/Tests/Unit/FArraySortedDictionaryTest.m @@ -0,0 +1,485 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +#import "FArraySortedDictionary.h" +#import "FTreeSortedDictionary.h" + +@interface FArraySortedDictionaryTests : XCTestCase + +@end + +@implementation FArraySortedDictionaryTests + +- (NSComparator) defaultComparator { + return ^(id obj1, id obj2) { + if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) { + return [obj1 compare:obj2]; + } + else { + if(obj1 < obj2) { + return (NSComparisonResult)NSOrderedAscending; + } + else if (obj1 > obj2) { + return (NSComparisonResult)NSOrderedDescending; + } + else { + return (NSComparisonResult)NSOrderedSame; + } + } + }; +} + + + +- (void)testCreateNode +{ + FImmutableSortedDictionary *map = [[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@"key" withValue:@"value"]; + XCTAssertEqual(map.count, 1, @"Contains one element"); +} + +- (void)testGetNilReturnsNil { + FImmutableSortedDictionary *map1 = [[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@"key" withValue:@"value"]; + XCTAssertNil([map1 get:nil]); + + FImmutableSortedDictionary *map2 = [[[FArraySortedDictionary alloc] initWithComparator:^NSComparisonResult(id obj1, id obj2) { + return [obj1 compare:obj2]; + }] + insertKey:@"key" withValue:@"value"]; + XCTAssertNil([map2 get:nil]); +} + +- (void)testSearchForSpecificKey { + FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2]; + + XCTAssertEqualObjects([map get:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map get:@2], @2, @"Found second object"); + XCTAssertNil([map get:@3], @"Properly not found object"); +} + +- (void)testRemoveKeyValuePair { + FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2]; + + FImmutableSortedDictionary* newMap = [map removeKey:@1]; + XCTAssertEqualObjects([newMap get:@2], @2, @"Found second object"); + XCTAssertNil([newMap get:@1], @"Properly not found object"); + + // Make sure the original one is not mutated + XCTAssertEqualObjects([map get:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map get:@2], @2, @"Found second object"); +} + +- (void)testMoreRemovals { + FImmutableSortedDictionary *map = [[[[[[[[[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@50 withValue:@50] + insertKey:@3 withValue:@3] + insertKey:@4 withValue:@4] + insertKey:@7 withValue:@7] + insertKey:@9 withValue:@9] + insertKey:@20 withValue:@20] + insertKey:@18 withValue:@18] + insertKey:@2 withValue:@2] + insertKey:@71 withValue:@71] + insertKey:@42 withValue:@42] + insertKey:@88 withValue:@88]; + XCTAssertNotNil([map get:@7], @"Found object"); + XCTAssertNotNil([map get:@3], @"Found object"); + XCTAssertNotNil([map get:@1], @"Found object"); + + + FImmutableSortedDictionary* m1 = [map removeKey:@7]; + FImmutableSortedDictionary* m2 = [map removeKey:@3]; + FImmutableSortedDictionary* m3 = [map removeKey:@1]; + + XCTAssertNil([m1 get:@7], @"Removed object"); + XCTAssertNotNil([m1 get:@3], @"Found object"); + XCTAssertNotNil([m1 get:@1], @"Found object"); + + XCTAssertNil([m2 get:@3], @"Removed object"); + XCTAssertNotNil([m2 get:@7], @"Found object"); + XCTAssertNotNil([m2 get:@1], @"Found object"); + + + XCTAssertNil([m3 get:@1], @"Removed object"); + XCTAssertNotNil([m3 get:@7], @"Found object"); + XCTAssertNotNil([m3 get:@3], @"Found object"); +} + +- (void) testRemovalBug { + FImmutableSortedDictionary *map = [[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2] + insertKey:@3 withValue:@3]; + + XCTAssertEqualObjects([map get:@1], @1, @"Found object"); + XCTAssertEqualObjects([map get:@2], @2, @"Found object"); + XCTAssertEqualObjects([map get:@3], @3, @"Found object"); + + FImmutableSortedDictionary* m1 = [map removeKey:@2]; + XCTAssertEqualObjects([m1 get:@1], @1, @"Found object"); + XCTAssertEqualObjects([m1 get:@3], @3, @"Found object"); + XCTAssertNil([m1 get:@2], @"Removed object"); +} + +- (void) testIncreasing { + int total = 20; + + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + for(int i = 0; i < total; i++) { + NSNumber* item = [NSNumber numberWithInt:i]; + map = [map insertKey:item withValue:item]; + } + + XCTAssertTrue([map count] == 20, @"Check if all 100 objects are in the map"); + + for(int i = 0; i < total; i++) { + NSNumber* item = [NSNumber numberWithInt:i]; + map = [map removeKey:item]; + } + + XCTAssertTrue([map count] == 0, @"Check if all 100 objects were removed"); +} + +- (void) testOverride { + FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@10 withValue:@10] + insertKey:@10 withValue:@8]; + + XCTAssertEqualObjects([map get:@10], @8, @"Found first object"); +} +- (void) testEmpty { + FImmutableSortedDictionary *map = [[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@10 withValue:@10] + removeKey:@10]; + + XCTAssertTrue([map isEmpty], @"Properly empty"); + +} + +- (void) testEmptyGet { + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertNil([map get:@"something"], @"Properly nil"); +} + +- (void) testEmptyCount { + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertTrue([map count] == 0, @"Properly zero count"); +} + +- (void) testEmptyRemoval { + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertTrue([[map removeKey:@"sometjhing"] count] == 0, @"Properly zero count"); +} + +- (void) testReverseTraversal { + FImmutableSortedDictionary *map = [[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@5 withValue:@5] + insertKey:@3 withValue:@3] + insertKey:@2 withValue:@2] + insertKey:@4 withValue:@4]; + + __block int next = 5; + [map enumerateKeysAndObjectsReverse:YES usingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Properly equal"); + next = next - 1; + }]; +} + + +- (void) testInsertionAndRemovalOfAHundredItems { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i]]; + [toRemove addObject:[NSNumber numberWithInt:i]]; + } + + + [self shuffleArray:toInsert]; + [self shuffleArray:toRemove]; + + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + + // check the order is correct + __block int next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key"); + XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual(next, N, @"Check we traversed all of the items"); + + // remove them + + for(int i = 0; i < N; i++) { + map = [map removeKey:[toRemove objectAtIndex:i]]; + } + + + XCTAssertEqual([map count], 0, @"Check we removed all of the items"); +} + +- (void) shuffleArray:(NSMutableArray *)array { + NSUInteger count = [array count]; + for(NSUInteger i = 0; i < count; i++) { + NSInteger nElements = count - i; + NSInteger n = (arc4random() % nElements) + i; + [array exchangeObjectAtIndex:i withObjectAtIndex:n]; + } +} + +- (void) testOrderIsCorrect { + + NSArray* toInsert = [[NSArray alloc] initWithObjects:@1,@7,@8,@5,@2,@6,@4,@0,@3, nil]; + + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < [toInsert count]; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == [toInsert count], @"Check if all N objects are in the map"); + + // check the order is correct + __block NSUInteger next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, [NSNumber numberWithInteger:next], @"Correct key"); + XCTAssertEqualObjects(value, [NSNumber numberWithInteger:next], @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual(next, [toInsert count], @"Check we traversed all of the items"); +} + +- (void) testPredecessorKey { + FImmutableSortedDictionary *map = [[[[[[[[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@50 withValue:@50] + insertKey:@3 withValue:@3] + insertKey:@4 withValue:@4] + insertKey:@7 withValue:@7] + insertKey:@9 withValue:@9]; + + XCTAssertNil([map getPredecessorKey:@1], @"First object doesn't have a predecessor"); + XCTAssertEqualObjects([map getPredecessorKey:@3], @1, @"@1"); + XCTAssertEqualObjects([map getPredecessorKey:@4], @3, @"@3"); + XCTAssertEqualObjects([map getPredecessorKey:@7], @4, @"@4"); + XCTAssertEqualObjects([map getPredecessorKey:@9], @7, @"@7"); + XCTAssertEqualObjects([map getPredecessorKey:@50], @9, @"@9"); + XCTAssertThrows([map getPredecessorKey:@777], @"Expect exception about nonexistant key"); +} + +- (void) testEnumerator { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary"); + + NSEnumerator* enumerator = [map keyEnumerator]; + id next = [enumerator nextObject]; + int correctValue = 0; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue + 1; + } +} + +- (void) testReverseEnumerator { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary"); + + NSEnumerator* enumerator = [map reverseKeyEnumerator]; + id next = [enumerator nextObject]; + int correctValue = N - 1; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue--; + } +} + +- (void) testEnumeratorFrom { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i*2]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary"); + + // Test from inbetween keys + { + NSEnumerator* enumerator = [map keyEnumeratorFrom:@11]; + id next = [enumerator nextObject]; + int correctValue = 12; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue + 2; + } + } + + // Test from key in map + { + NSEnumerator* enumerator = [map keyEnumeratorFrom:@10]; + id next = [enumerator nextObject]; + int correctValue = 10; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue + 2; + } + } +} + +- (void) testReverseEnumeratorFrom { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i*2]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *map = [[FArraySortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:[FArraySortedDictionary class]], @"Make sure we still have a array backed dictionary"); + + // Test from inbetween keys + { + NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@11]; + id next = [enumerator nextObject]; + int correctValue = 10; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue - 2; + } + } + + // Test from key in map + { + NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@10]; + id next = [enumerator nextObject]; + int correctValue = 10; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue - 2; + } + } +} + +- (void)testConversionToTreeMap { + int N = SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD + 5; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *dict = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]]; + + for(int i = 0; i < N; i++) { + dict = [dict insertKey:toInsert[i] withValue:toInsert[i]]; + if (i < SORTED_DICTIONARY_ARRAY_TO_RB_TREE_SIZE_THRESHOLD) { + XCTAssertTrue([dict isKindOfClass:[FArraySortedDictionary class]], + @"We're below the threshold we should be an array backed implementation"); + XCTAssertEqual(dict.count, i + 1, @"Size doesn't match"); + } else { + XCTAssertTrue([dict isKindOfClass:[FTreeSortedDictionary class]], + @"We're above the threshold we should be a tree backed implementation"); + XCTAssertEqual(dict.count, i + 1, @"Size doesn't match"); + } + } + + // check the order is correct + __block NSUInteger next = 0; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, [NSNumber numberWithInteger:next], @"Correct key"); + XCTAssertEqualObjects(value, [NSNumber numberWithInteger:next], @"Correct value"); + next = next + 1; + }]; +} + + + +@end + diff --git a/Example/Database/Tests/Unit/FCompoundHashTest.m b/Example/Database/Tests/Unit/FCompoundHashTest.m new file mode 100644 index 0000000..15e6d10 --- /dev/null +++ b/Example/Database/Tests/Unit/FCompoundHashTest.m @@ -0,0 +1,141 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import "FCompoundHash.h" +#import "FTestHelpers.h" +#import "FEmptyNode.h" +#import "FStringUtilities.h" +#import "FEmptyNode.h" + +@interface FCompoundHashTest : XCTestCase + +@end + +@implementation FCompoundHashTest + +static FCompoundHashSplitStrategy NEVER_SPLIT_STRATEGY = ^BOOL(FCompoundHashBuilder *builder) { + return NO; +}; + +- (FCompoundHashSplitStrategy)splitAtPaths:(NSArray *)paths { + return ^BOOL(FCompoundHashBuilder *builder) { + return [paths containsObject:builder.currentPath]; + }; +} + +- (void)testEmptyNodeYieldsEmptyHash { + FCompoundHash *hash = [FCompoundHash fromNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(hash.posts, @[]); + XCTAssertEqualObjects(hash.hashes, @[@""]); +} + +- (void)testCompoundHashIsAlwaysFollowedByEmptyHash { + id<FNode> node = NODE(@{@"foo": @"bar"}); + FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY]; + NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(string:\"bar\"))"]; + + XCTAssertEqualObjects(hash.posts, @[PATH(@"foo")]); + XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""])); +} + +- (void)testCompoundHashCanSplitAtPriority { + id<FNode> node = NODE((@{@"foo": @{@"!beforePriority": @"before", @".priority": @"prio", @"afterPriority": @"after"}, @"qux": @"qux"})); + FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"foo/.priority")]]]; + NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"!beforePriority\":(string:\"before\"),\".priority\":(string:\"prio\")))"]; + NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"afterPriority\":(string:\"after\")),\"qux\":(string:\"qux\"))"]; + XCTAssertEqualObjects(hash.posts, (@[PATH(@"foo/.priority"), PATH(@"qux")])); + XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""])); +} + +- (void)testHashesPriorityLeafNodes { + id<FNode> node = NODE((@{@"foo": @{@".value": @"bar", @".priority": @"baz"}})); + FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY]; + NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(priority:string:\"baz\":string:\"bar\"))"]; + + XCTAssertEqualObjects(hash.posts, @[PATH(@"foo")]); + XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""])); +} + +- (void)testHashingFollowsFirebaseKeySemantics { + id<FNode> node = NODE((@{@"1": @"one", @"2": @"two", @"10": @"ten"})); + // 10 is after 2 in Firebase key semantics, but would be before 2 in string semantics + FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"2")]]]; + NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"1\":(string:\"one\"),\"2\":(string:\"two\"))"]; + NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"10\":(string:\"ten\"))"]; + XCTAssertEqualObjects(hash.posts, (@[PATH(@"2"), PATH(@"10")])); + XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""])); +} + +- (void)testHashingOnChildBoundariesWorks { + id<FNode> node = NODE((@{@"bar": @{@"deep": @"value"}, @"foo": @{@"other-deep": @"value"}})); + FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:[self splitAtPaths:@[PATH(@"bar/deep")]]]; + NSString *firstHash = [FStringUtilities base64EncodedSha1:@"(\"bar\":(\"deep\":(string:\"value\")))"]; + NSString *secondHash = [FStringUtilities base64EncodedSha1:@"(\"foo\":(\"other-deep\":(string:\"value\")))"]; + XCTAssertEqualObjects(hash.posts, (@[PATH(@"bar/deep"), PATH(@"foo/other-deep")])); + XCTAssertEqualObjects(hash.hashes, (@[firstHash, secondHash, @""])); +} + +- (void)testCommasAreSetForNestedChildren { + id<FNode> node = NODE((@{@"bar": @{@"deep": @"value"}, @"foo": @{@"other-deep": @"value"}})); + FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY]; + NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"bar\":(\"deep\":(string:\"value\")),\"foo\":(\"other-deep\":(string:\"value\")))"]; + + XCTAssertEqualObjects(hash.posts, @[PATH(@"foo/other-deep")]); + XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""])); +} + +- (void)testQuotedStringsAndKeys { + id<FNode> node = NODE((@{@"\"": @"\\", @"\"\\\"\\": @"\"\\\"\\"})); + FCompoundHash *hash = [FCompoundHash fromNode:node splitStrategy:NEVER_SPLIT_STRATEGY]; + NSString *expectedHash = [FStringUtilities base64EncodedSha1:@"(\"\\\"\":(string:\"\\\\\"),\"\\\"\\\\\\\"\\\\\":(string:\"\\\"\\\\\\\"\\\\\"))"]; + + XCTAssertEqualObjects(hash.posts, @[PATH(@"\"\\\"\\")]); + XCTAssertEqualObjects(hash.hashes, (@[expectedHash, @""])); +} + +- (void)testDefaultSplitHasSensibleAmountOfHashes { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + for (int i = 0; i < 500; i++) { + // roughly 15-20 bytes serialized per node, 10k total + dict[[NSString stringWithFormat:@"%d", i]] = @"value"; + } + id<FNode> node10k = NODE(dict); + + dict = [NSMutableDictionary dictionary]; + for (int i = 0; i < 5000; i++) { + // roughly 15-20 bytes serialized per node, 100k total + dict[[NSString stringWithFormat:@"%d", i]] = @"value"; + } + id<FNode> node100k = NODE(dict); + + dict = [NSMutableDictionary dictionary]; + for (int i = 0; i < 50000; i++) { + // roughly 15-20 bytes serialized per node, 1M total + dict[[NSString stringWithFormat:@"%d", i]] = @"value"; + } + id<FNode> node1M = NODE(dict); + + FCompoundHash *hash10k = [FCompoundHash fromNode:node10k]; + FCompoundHash *hash100k = [FCompoundHash fromNode:node100k]; + FCompoundHash *hash1M = [FCompoundHash fromNode:node1M]; + XCTAssertEqualWithAccuracy(hash10k.hashes.count, 15, 3); + XCTAssertEqualWithAccuracy(hash100k.hashes.count, 50, 5); + XCTAssertEqualWithAccuracy(hash1M.hashes.count, 150, 10); +} + +@end diff --git a/Example/Database/Tests/Unit/FCompoundWriteTest.m b/Example/Database/Tests/Unit/FCompoundWriteTest.m new file mode 100644 index 0000000..1e0a85e --- /dev/null +++ b/Example/Database/Tests/Unit/FCompoundWriteTest.m @@ -0,0 +1,526 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FNode.h" +#import "FSnapshotUtilities.h" +#import "FCompoundWrite.h" +#import "FEmptyNode.h" +#import "FLeafNode.h" +#import "FNamedNode.h" + +@interface FCompoundWriteTest : XCTestCase + +@end + +@implementation FCompoundWriteTest + +- (id<FNode>) leafNode { + static id<FNode> node = nil; + if (!node) { + node = [FSnapshotUtilities nodeFrom:@"leaf-node"]; + } + return node; +} + +- (id<FNode>) priorityNode { + static id<FNode> node = nil; + if (!node) { + node = [FSnapshotUtilities nodeFrom:@"prio"]; + } + return node; +} + +- (id<FNode>) baseNode { + static id<FNode> node = nil; + if (!node) { + NSDictionary *base = @{@"child-1" : @"value-1", @"child-2" : @"value-2"}; + node = [FSnapshotUtilities nodeFrom:base]; + } + return node; +} + +- (void) assertAppliedCompoundWrite:(FCompoundWrite *)compoundWrite equalsNode:(id<FNode>)node withPriority:(id<FNode>)priority { + id<FNode> updatedNode = [compoundWrite applyToNode:node]; + if (node.isEmpty) { + XCTAssertEqualObjects([FEmptyNode emptyNode], updatedNode, + @"Applied compound write should be empty. %@", updatedNode); + } else { + XCTAssertEqualObjects([node updatePriority:priority], updatedNode, + @"Applied compound write should equal node with priority. %@", updatedNode); + } +} + +- (void) testEmptyMergeIsEmpty { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + XCTAssertTrue(compoundWrite.isEmpty, @"Empty write should be empty %@", compoundWrite); +} + +- (void) testCompoundWriteWithPriorityUpdateIsNotEmpty { + FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.priorityNode atKey:@".priority"]; + XCTAssertFalse(compoundWrite.isEmpty, @"Priority update should not be empty %@", compoundWrite); +} + +- (void) testCompoundWriteWithUpdateIsNotEmpty { + FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.leafNode + atPath:[[FPath alloc] initWith:@"foo/bar"]]; + XCTAssertFalse(compoundWrite.isEmpty, @"Update should not be empty %@", compoundWrite); +} + +- (void) testCompoundWriteWithRootUpdateIsNotEmpty { + FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.leafNode + atPath:[FPath empty]]; + XCTAssertFalse(compoundWrite.isEmpty, @"Update at root should not be empty %@", compoundWrite); +} + +- (void) testCompoundWriteWithEmptyRootUpdateIsNotEmpty { + FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:[FEmptyNode emptyNode] + atPath:[FPath empty]]; + XCTAssertFalse(compoundWrite.isEmpty, @"Empty root update should not be empty %@", compoundWrite); +} + +- (void) testCompoundWriteWithRootPriorityUpdateAndChildMergeIsNotEmpty { + FCompoundWrite *compoundWrite = [[FCompoundWrite emptyWrite] addWrite:self.priorityNode atKey:@".priority"]; + compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@".priority"]]; + XCTAssertFalse(compoundWrite.isEmpty, @"Compound write with root priority update and child merge should not be empty."); +} + +- (void) testAppliesLeafOverwrite { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, self.leafNode, @"Should get leaf node once applied %@", updatedNode); +} + +- (void) testAppliesChildrenOverwrite { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> childNode = [[FEmptyNode emptyNode] updateImmediateChild:@"child" withNewChild:self.leafNode]; + compoundWrite = [compoundWrite addWrite:childNode atPath:[FPath empty]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, childNode, @"Child overwrite should work"); +} + +- (void) testAddsChildNode { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> expectedNode = [[FEmptyNode emptyNode] updateImmediateChild:@"child" withNewChild:self.leafNode]; + compoundWrite = [compoundWrite addWrite:self.leafNode atKey:@"child"]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding child node should work %@", updatedNode); +} + +- (void) testAddsDeepChildNode { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + FPath *path = [[FPath alloc] initWith:@"deep/deep/node"]; + id<FNode> expectedNode = [[FEmptyNode emptyNode] updateChild:path withNewChild:self.leafNode]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Should add deep child node correctly"); +} + +- (void) testOverwritesExistingChild { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + FPath *path = [[FPath alloc] initWith:@"child-1"]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + id<FNode> expectedNode = [self.baseNode updateImmediateChild:[path getFront] withNewChild:self.leafNode]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Overwriting existing child should work."); +} + +- (void) testUpdatesExistingChild { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + FPath *path = [[FPath alloc] initWith:@"child-1/foo"]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:path]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + id<FNode> expectedNode = [self.baseNode updateChild:path withNewChild:self.leafNode]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Updating existing child should work"); +} + +- (void) testDoesntUpdatePriorityOnEmptyNode { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atKey:@".priority"]; + [self assertAppliedCompoundWrite:compoundWrite equalsNode:[FEmptyNode emptyNode] withPriority:[FEmptyNode emptyNode]]; +} + +- (void) testUpdatesPriorityOnNode { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atKey:@".priority"]; + id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"]; + [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:self.priorityNode]; +} + +- (void) testUpdatesPriorityOfChild { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + FPath *path = [[FPath alloc] initWith:@"child-1/.priority"]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:path]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + id<FNode> expectedNode = [self.baseNode updateChild:path withNewChild:self.priorityNode]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Updating priority of child should work."); +} + +- (void) testDoesntUpdatePriorityOfNonExistentChild { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + FPath *path = [[FPath alloc] initWith:@"child-3/.priority"]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:path]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + XCTAssertEqualObjects(updatedNode, self.baseNode, @"Should not update priority of nonexistent child"); +} + +- (void) testDeepUpdateExistingUpdates { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"]; + id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"]; + compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]]; + compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]]; + compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]]; + NSDictionary *expectedChild1 = @{@"foo":@"new-foo-value", @"bar":@"bar-value", @"baz":@"baz-value"}; + id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep update with existing updates should work."); +} + +- (void) testShallowUpdateRemovesDeepUpdate { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update1 = [FSnapshotUtilities nodeFrom:@"new-foo-value"]; + id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"]; + id<FNode> update3 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1/foo"]]; + compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]]; + compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1"]]; + NSDictionary *expectedChild1 = @{@"foo":@"foo-value", @"bar":@"bar-value"}; + id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Shallow update should remove deep udpates."); +} + +- (void) testChildPriorityDoesntUpdateEmptyNodePriorityOnChildMerge { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child-1/.priority"]]; + compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]]; + [self assertAppliedCompoundWrite:compoundWrite equalsNode:[FEmptyNode emptyNode] withPriority:[FEmptyNode emptyNode]]; +} + +- (void) testChildPriorityUpdatesPriorityOnChildMerge { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child-1/.priority"]]; + id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"]; + compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]]; + [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:self.priorityNode]; +} + +- (void) testChildPriorityUpdatesEmptyPriorityOnChildMerge { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-1/.priority"]]; + id<FNode> node = [[FLeafNode alloc] initWithValue:@"foo" withPriority:self.priorityNode]; + compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"child-1"]]; + [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:[FEmptyNode emptyNode]]; +} + +- (void) testDeepPrioritySetWorksOnEmptyNodeWhenOtherSetIsAvailable { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"foo/.priority"]]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"foo/child"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + id<FNode> updatedPriority = [updatedNode getChild:[[FPath alloc] initWith:@"foo"]].getPriority; + XCTAssertEqualObjects(updatedPriority, self.priorityNode, @"Should get priority"); +} + +- (void) testChildMergeLooksIntoUpdateNode { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]]; + compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"foo"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:@"foo-value"]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Child merge should get updates."); +} + +- (void) testChildMergeRemovesNodeOnDeeperPaths { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]]; + compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@"foo/not/existing"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.leafNode]; + id<FNode> expectedNode = [FEmptyNode emptyNode]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Should not have node."); +} + +- (void) testChildMergeWithEmptyPathIsSameMerge { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]]; + XCTAssertEqualObjects([compoundWrite childCompoundWriteAtPath:[FPath empty]], compoundWrite, + @"Child merge with empty path should be the same merge."); +} + +- (void) testRootUpdateRemovesRootPriority { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]]; + id<FNode> update = [FSnapshotUtilities nodeFrom:@"foo"]; + compoundWrite = [compoundWrite addWrite:update atPath:[FPath empty]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, update, @"Root update should remove root priority"); +} + +- (void) testDeepUpdateRemovesPriorityThere { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"foo/.priority"]]; + id<FNode> update = [FSnapshotUtilities nodeFrom:@"bar"]; + compoundWrite = [compoundWrite addWrite:update atPath:[[FPath alloc] initWith:@"foo"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:@{@"foo":@"bar"}]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep update should remove priority there"); +} + +- (void) testAddingUpdatesAtPathWorks { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init]; + [updateDictionary setObject:@"foo-value" forKey:@"foo"]; + [updateDictionary setObject:@"bar-value" forKey:@"bar"]; + FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary]; + compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[[FPath alloc] initWith:@"child-1"]]; + + NSDictionary *expectedChild1 = @{@"foo":@"foo-value", @"bar":@"bar-value"}; + id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expectedChild1]]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding updates at a path should work."); +} + +- (void) testAddingUpdatesAtRootWorks { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init]; + [updateDictionary setObject:@"new-value-1" forKey:@"child-1"]; + [updateDictionary setObject:[NSNull null] forKey:@"child-2"]; + [updateDictionary setObject:@"value-3" forKey:@"child-3"]; + FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary]; + compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[FPath empty]]; + + NSDictionary *expected = @{@"child-1":@"new-value-1", @"child-3":@"value-3"}; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:expected]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Adding updates at root should work."); +} + +- (void) testChildMergeOfRootPriorityWorks { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]]; + compoundWrite = [compoundWrite childCompoundWriteAtPath:[[FPath alloc] initWith:@".priority"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, self.priorityNode, @"Child merge of root priority should work."); +} + +- (void) testCompleteChildrenOnlyReturnsCompleteOverwrites { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1"]]; + NSArray *expectedChildren = @[[[FNamedNode alloc] initWithName:@"child-1" andNode:self.leafNode]]; + NSArray *completeChildren = [compoundWrite completeChildren]; + XCTAssertEqualObjects(completeChildren, expectedChildren, @"Complete children should only return on complete overwrites."); +} + +- (void) testCompleteChildrenOnlyReturnsEmptyOverwrites { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-1"]]; + NSArray *expectedChildren = @[[[FNamedNode alloc] initWithName:@"child-1" andNode:[FEmptyNode emptyNode]]]; + NSArray *completeChildren = [compoundWrite completeChildren]; + XCTAssertEqualObjects(completeChildren, expectedChildren, @"Complete children should return list with empty on empty overwrites."); +} + +- (void) testCompleteChildrenDoesntReturnDeepOverwrites { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1/deep/path"]]; + NSArray *expectedChildren = @[]; + NSArray *completeChildren = [compoundWrite completeChildren]; + XCTAssertEqualObjects(completeChildren, expectedChildren, @"Should not get complete children on deep overwrites."); +} + +- (void) testCompleteChildrenReturnAllCompleteChildrenButNoIncomplete { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-1/deep/path"]]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"child-2"]]; + compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child-3"]]; + NSDictionary *expected = @{ + @"child-2":self.leafNode, + @"child-3":[FEmptyNode emptyNode] + }; + NSMutableDictionary *actual = [[NSMutableDictionary alloc] init]; + for (FNamedNode *node in compoundWrite.completeChildren) { + [actual setObject:node.node forKey:node.name]; + } + XCTAssertEqualObjects(actual, expected, @"Complete children should get returned, but not incomplete ones."); +} + +- (void) testCompleteChildrenReturnAllChildrenForRootSet { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.baseNode atPath:[FPath empty]]; + + NSDictionary *expected = @{ + @"child-1": [FSnapshotUtilities nodeFrom:@"value-1"], + @"child-2": [FSnapshotUtilities nodeFrom:@"value-2"] + }; + + NSMutableDictionary *actual = [[NSMutableDictionary alloc] init]; + for (FNamedNode *node in compoundWrite.completeChildren) { + [actual setObject:node.node forKey:node.name]; + } + XCTAssertEqualObjects(actual, expected, @"Complete children should return all children on root set."); +} + +- (void) testEmptyMergeHasNoShadowingWrite { + XCTAssertFalse([[FCompoundWrite emptyWrite] hasCompleteWriteAtPath:[FPath empty]], @"Empty merge has no shadowing write."); +} + +- (void) testCompoundWriteWithEmptyRootHasShadowingWrite { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[FPath empty]]; + XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Empty write should have shadowing write at root."); + XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"child"]], @"Empty write should have complete write at child."); +} + +- (void) testCompoundWriteWithRootHasShadowingWrite { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]]; + XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Root write should have shadowing write at root."); + XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"child"]], @"Root write should have complete write at child."); +} + +- (void) testCompoundWriteWithDeepUpdateHasShadowingWrite { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[[FPath alloc] initWith:@"deep/update"]]; + XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Deep write should not have complete write at root."); + XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"deep"]], @"Deep write should not have should have complete write at child."); + XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@"deep/update"]], @"Deep write should have complete write at deep child."); +} + +- (void) testCompoundWriteWithPriorityUpdateHasShadowingWrite { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]]; + XCTAssertFalse([compoundWrite hasCompleteWriteAtPath:[FPath empty]], @"Write with priority at root should not have complete write at root."); + XCTAssertTrue([compoundWrite hasCompleteWriteAtPath:[[FPath alloc] initWith:@".priority"]], @"Write with priority at root should have complete priority."); +} + +- (void) testUpdatesCanBeRemoved { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + compoundWrite = [compoundWrite addWrite:update atPath:[[FPath alloc] initWith:@"child-1"]]; + compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + XCTAssertEqualObjects(updatedNode, self.baseNode, @"Updates should be removed."); +} + +- (void) testDeepRemovesHasNoEffectOnOverlayingSet { + // TODO I don't get this one. + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"]; + id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"]; + compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]]; + compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]]; + compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]]; + compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1/foo"]]; + NSDictionary *expected = @{ + @"foo":@"new-foo-value", + @"bar":@"bar-value", + @"baz":@"baz-value" + }; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expected]]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Deep removes should have no effect on overlaying set."); +} + +- (void) testRemoveAtPathWithoutSetIsWithoutEffect { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update1 = [FSnapshotUtilities nodeFrom:@{@"foo":@"foo-value", @"bar":@"bar-value"}]; + id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"]; + id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"]; + compoundWrite = [compoundWrite addWrite:update1 atPath:[[FPath alloc] initWith:@"child-1"]]; + compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]]; + compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]]; + compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-2"]]; + NSDictionary *expected = @{ + @"foo":@"new-foo-value", + @"bar":@"bar-value", + @"baz":@"baz-value" + }; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + id<FNode> expectedNode = [self.baseNode updateImmediateChild:@"child-1" withNewChild:[FSnapshotUtilities nodeFrom:expected]]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Removing at path without a set should have no effect."); +} + +- (void) testCanRemovePriority { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@".priority"]]; + compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@".priority"]]; + [self assertAppliedCompoundWrite:compoundWrite equalsNode:self.leafNode withPriority:[FEmptyNode emptyNode]]; +} + +- (void) testRemovingOnlyAffectsRemovedPath { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + NSDictionary *updateDictionary = @{ + @"child-1": @"new-value-1", + @"child-2": [NSNull null], + @"child-3": @"value-3" + }; + FCompoundWrite *updates = [FCompoundWrite compoundWriteWithValueDictionary:updateDictionary]; + compoundWrite = [compoundWrite addCompoundWrite:updates atPath:[FPath empty]]; + compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-2"]]; + + NSDictionary *expected = @{ + @"child-1": @"new-value-1", + @"child-2": @"value-2", + @"child-3": @"value-3" + }; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + id<FNode> expectedNode = [FSnapshotUtilities nodeFrom:expected]; + XCTAssertEqualObjects(updatedNode, expectedNode, @"Removing should only affected removed paths"); +} + +- (void) testRemoveRemovesAllDeeperSets { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + id<FNode> update2 = [FSnapshotUtilities nodeFrom:@"baz-value"]; + id<FNode> update3 = [FSnapshotUtilities nodeFrom:@"new-foo-value"]; + compoundWrite = [compoundWrite addWrite:update2 atPath:[[FPath alloc] initWith:@"child-1/baz"]]; + compoundWrite = [compoundWrite addWrite:update3 atPath:[[FPath alloc] initWith:@"child-1/foo"]]; + compoundWrite = [compoundWrite removeWriteAtPath:[[FPath alloc] initWith:@"child-1"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:self.baseNode]; + XCTAssertEqualObjects(updatedNode, self.baseNode, @"Remove should remove deeper sets."); +} + +- (void) testRemoveAtRootAlsoRemovesPriority { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:[[FLeafNode alloc] initWithValue:@"foo" withPriority:self.priorityNode] atPath:[FPath empty]]; + compoundWrite = [compoundWrite removeWriteAtPath:[FPath empty]]; + id<FNode> node = [FSnapshotUtilities nodeFrom:@"value"]; + [self assertAppliedCompoundWrite:compoundWrite equalsNode:node withPriority:[FEmptyNode emptyNode]]; +} + +- (void) testUpdatingPriorityDoesntOverwriteLeafNode { + // TODO I don't get this one. + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]]; + compoundWrite = [compoundWrite addWrite:self.priorityNode atPath:[[FPath alloc] initWith:@"child/.priority"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, self.leafNode, @"Updating priority should not overwrite leaf node."); +} + +- (void) testUpdatingEmptyChildNodeDoesntOverwriteLeafNode { + FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite]; + compoundWrite = [compoundWrite addWrite:self.leafNode atPath:[FPath empty]]; + compoundWrite = [compoundWrite addWrite:[FEmptyNode emptyNode] atPath:[[FPath alloc] initWith:@"child"]]; + id<FNode> updatedNode = [compoundWrite applyToNode:[FEmptyNode emptyNode]]; + XCTAssertEqualObjects(updatedNode, self.leafNode, @"Updating empty node should not overwrite leaf node."); +} + +@end diff --git a/Example/Database/Tests/Unit/FIRDataSnapshotTests.h b/Example/Database/Tests/Unit/FIRDataSnapshotTests.h new file mode 100644 index 0000000..b69e7f2 --- /dev/null +++ b/Example/Database/Tests/Unit/FIRDataSnapshotTests.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +@interface FIRDataSnapshotTests : XCTestCase + +@end diff --git a/Example/Database/Tests/Unit/FIRDataSnapshotTests.m b/Example/Database/Tests/Unit/FIRDataSnapshotTests.m new file mode 100644 index 0000000..2a442df --- /dev/null +++ b/Example/Database/Tests/Unit/FIRDataSnapshotTests.m @@ -0,0 +1,449 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRDataSnapshotTests.h" +#import "FIRDatabaseConfig_Private.h" +#import "FTestHelpers.h" +#import "FLeafNode.h" +#import "FChildrenNode.h" +#import "FEmptyNode.h" +#import "FImmutableSortedDictionary.h" +#import "FUtilities.h" +#import "FSnapshotUtilities.h" +#import "FIRDatabaseReference.h" +#import "FIRDataSnapshot_Private.h" +#import "FPathIndex.h" +#import "FLeafNode.h" +#import "FValueIndex.h" + +@implementation FIRDataSnapshotTests + +- (void)setUp +{ + [super setUp]; + + // Set-up code here. +} + +- (void)tearDown +{ + // Tear-down code here. + + [super tearDown]; +} + +- (FIRDataSnapshot *)snapshotFor:(id)jsonDict { + FIRDatabaseConfig *config = [FIRDatabaseConfig defaultConfig]; + FRepoInfo* repoInfo = [[FRepoInfo alloc] initWithHost:@"example.com" isSecure:NO withNamespace:@"default"]; + FIRDatabaseReference * dummyRef = [[FIRDatabaseReference alloc] initWithRepo:[FRepoManager getRepo:repoInfo config:config] path:[FPath empty]]; + FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:[FSnapshotUtilities nodeFrom:jsonDict]]; + FIRDataSnapshot * snapshot = [[FIRDataSnapshot alloc] initWithRef:dummyRef indexedNode:indexed]; + return snapshot; +} + +- (void) testCreationLeafNodesVariousTypes { + + id<FNode> fortyTwo = [FSnapshotUtilities nodeFrom:@42]; + FLeafNode* x = [[FLeafNode alloc] initWithValue:@5 withPriority:fortyTwo]; + + XCTAssertEqualObjects(x.val, @5, @"Values are the same"); + XCTAssertEqualObjects(x.getPriority, [FSnapshotUtilities nodeFrom:@42], @"Priority is the same"); + XCTAssertTrue([x isLeafNode], @"Node is a leaf"); + + x = [[FLeafNode alloc] initWithValue:@"test"]; + XCTAssertEqualObjects(x.value, @"test", @"Check if leaf node is holding onto a string value"); + + x = [[FLeafNode alloc] initWithValue:[NSNumber numberWithBool:YES]]; + XCTAssertTrue([x.value boolValue], @"Check if leaf node is holding onto a YES boolean"); + + x = [[FLeafNode alloc] initWithValue:[NSNumber numberWithBool:NO]]; + XCTAssertFalse([x.value boolValue], @"Check if leaf node is holding onto a NO boolean"); +} + +- (void) testUpdatingPriorityWithoutChangingOld { + FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test" withPriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]]]; + FLeafNode* y = [x updatePriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:187]]]; + + // old node is the same + XCTAssertEqualObjects(x.value, @"test", @"Values of old node are the same"); + XCTAssertEqualObjects(x.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]], @"Priority of old node is the same."); + + // new node has the new priority but the old value + XCTAssertEqualObjects(y.value, @"test", @"Values of old node are the same"); + XCTAssertEqualObjects(y.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:187]], @"Priority of new node is update"); +} + +- (void) testUpdateImmediateChildReturnsANewChildrenNode { + FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test" withPriority:[FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]]]; + FChildrenNode* y = [x updateImmediateChild:@"test" withNewChild:[[FLeafNode alloc] initWithValue:@"foo"]]; + + XCTAssertFalse([y isLeafNode], @"New node is no longer a leaf"); + XCTAssertEqualObjects(y.getPriority, [FSnapshotUtilities nodeFrom:[NSNumber numberWithInt:42]], @"Priority of new node is update"); + + XCTAssertEqualObjects([[y getImmediateChild:@"test"] val], @"foo", @"Child node has the correct value"); +} + +- (void) testGetImmediateChildOnLeafNode { + FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test"]; + XCTAssertEqualObjects([x getImmediateChild:@"foo"], [FEmptyNode emptyNode], @"Get immediate child on leaf node returns empty node"); +} + +- (void) testGetChildReturnsEmptyNode { + FLeafNode* x = [[FLeafNode alloc] initWithValue:@"test"]; + XCTAssertEqualObjects([x getChild:[[FPath alloc] initWith:@"foo/bar"]], [FEmptyNode emptyNode], @"Get child returns an empty node."); +} + +- (NSComparator) defaultComparator { + return ^(id obj1, id obj2) { + if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) { + return [obj1 compare:obj2]; + } + else { + if(obj1 < obj2) { + return (NSComparisonResult)NSOrderedAscending; + } + else if (obj1 > obj2) { + return (NSComparisonResult)NSOrderedDescending; + } + else { + return (NSComparisonResult)NSOrderedSame; + } + } + }; +} + +- (void) testUpdateImmediateChildWithNewNode { + FImmutableSortedDictionary* children = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]]; + FChildrenNode* x = [[FChildrenNode alloc] initWithChildren:children]; + FLeafNode* newValue = [[FLeafNode alloc] initWithValue:@"new value"]; + FChildrenNode* y = [x updateImmediateChild:@"test" withNewChild:newValue]; + + XCTAssertEqualObjects(x.children, children, @"Original object stays the same"); + XCTAssertEqualObjects([y.children objectForKey:@"test"], newValue, @"New internal node with the proper new value"); + XCTAssertEqualObjects([[y.children objectForKey:@"test"] val], @"new value", @"Check the payload"); +} + +- (void) testUpdatechildWithNewNode { + FImmutableSortedDictionary* children = [FImmutableSortedDictionary dictionaryWithComparator:[self defaultComparator]]; + FChildrenNode* x = [[FChildrenNode alloc] initWithChildren:children]; + FLeafNode* newValue = [[FLeafNode alloc] initWithValue:@"new value"]; + FChildrenNode* y = [x updateChild:[[FPath alloc] initWith:@"test/foo"] withNewChild:newValue]; + XCTAssertEqualObjects(x.children, children, @"Original object stays the same"); + XCTAssertEqualObjects([y getChild:[[FPath alloc] initWith:@"test/foo"]], newValue, @"Check if the updateChild held"); + XCTAssertEqualObjects([[y getChild:[[FPath alloc] initWith:@"test/foo"]] val], @"new value", @"Check the payload"); +} + +- (void) testObjectTypes { + XCTAssertEqualObjects(@"string", [FUtilities getJavascriptType:@""], @"Check string type"); + XCTAssertEqualObjects(@"string", [FUtilities getJavascriptType:@"moo"], @"Check string type"); + + XCTAssertEqualObjects(@"boolean", [FUtilities getJavascriptType:@YES], @"Check boolean type"); + XCTAssertEqualObjects(@"boolean", [FUtilities getJavascriptType:@NO], @"Check boolean type"); + + XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@5], @"Check number type"); + XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@5.5], @"Check number type"); + XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@0], @"Check number type"); + XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@8273482734], @"Check number type"); + XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@-2], @"Check number type"); + XCTAssertEqualObjects(@"number", [FUtilities getJavascriptType:@-2.11], @"Check number type"); +} + +- (void) testNodeHashWorksCorrectly { + id<FNode> node = [FSnapshotUtilities nodeFrom:@{ @"intNode" : @4, + @"doubleNode" : @4.5623, + @"stringNode" : @"hey guys", + @"boolNode" : @YES }]; + + XCTAssertEqualObjects(@"eVih19a6ZDz3NL32uVBtg9KSgQY=", [[node getImmediateChild:@"intNode"] dataHash], @"Check integer node"); + XCTAssertEqualObjects(@"vf1CL0tIRwXXunHcG/irRECk3lY=", [[node getImmediateChild:@"doubleNode"] dataHash], @"Check double node"); + XCTAssertEqualObjects(@"CUNLXWpCVoJE6z7z1vE57lGaKAU=", [[node getImmediateChild:@"stringNode"] dataHash], @"Check string node"); + XCTAssertEqualObjects(@"E5z61QM0lN/U2WsOnusszCTkR8M=", [[node getImmediateChild:@"boolNode"] dataHash], @"Check boolean node"); + XCTAssertEqualObjects(@"6Mc4jFmNdrLVIlJJjz2/MakTK9I=", [node dataHash], @"Check compound node"); +} + +- (void) testNodeHashWorksCorrectlyWithPriorities { + id<FNode> node = [FSnapshotUtilities nodeFrom:@{ + @"root": @{ @"c": @{@".value": @99, @".priority": @"abc"}, @".priority" : @"def" } + }]; + + XCTAssertEqualObjects(@"Fm6tzN4CVEu5WxFDZUdTtqbTVaA=", [node dataHash], @"Check compound node"); +} + +- (void) testGetPredecessorChild { + id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"d": @YES, @"a": @YES, @"g": @YES, @"c": @YES, @"e": @YES}]; + + XCTAssertNil([node predecessorChildKey:@"a"], + @"Check the first one sorted properly"); + XCTAssertEqualObjects([node predecessorChildKey:@"c"], + @"a", @"Check a comes before c"); + XCTAssertEqualObjects([node predecessorChildKey:@"d"], + @"c", @"Check c comes before d"); + XCTAssertEqualObjects([node predecessorChildKey:@"e"], + @"d", @"Check d comes before e"); + XCTAssertEqualObjects([node predecessorChildKey:@"g"], + @"e", @"Check e comes before g"); +} + +- (void) testSortedChildrenGetPredecessorChildWorksCorrectly { + // XXX impl SortedChildren +} + +- (void) testSortedChildrenUpdateImmediateChildrenWorksCorrectly { + // XXX imple SortedChildren +} + +- (void) testDataSnapshotHasChildrenWorks { + + FIRDataSnapshot * snap = [self snapshotFor:@{}]; + XCTAssertFalse([snap hasChildren], @"Empty dict has no children"); + + snap = [self snapshotFor:@5]; + XCTAssertFalse([snap hasChildren], @"Leaf node has no children"); + + snap = [self snapshotFor:@{@"x": @5}]; + XCTAssertTrue([snap hasChildren], @"Properly has children"); +} + +- (void) testDataSnapshotValWorks { + FIRDataSnapshot * snap = [self snapshotFor:@5]; + XCTAssertEqualObjects([snap value], @5, @"Leaf node values are correct"); + + snap = [self snapshotFor:@{}]; + XCTAssertTrue([snap value] == [NSNull null], @"Snapshot value is properly null"); + + NSDictionary* dict = @{ + @"x": @5, + @"y": @{ + @"ya": @1, + @"yb": @2, + @"yc": @{ @"yca" : @3} + } + }; + + snap = [self snapshotFor:dict]; + XCTAssertTrue([dict isEqualToDictionary:[snap value]], @"Check if the dictionaries are the same"); +} + +- (void) testDataSnapshotChildWorks { + FIRDataSnapshot * snap = [self snapshotFor:@{@"x": @5, @"y": @{@"yy": @3, @"yz": @4}}]; + + XCTAssertEqualObjects([[snap childSnapshotForPath:@"x"] value], @5, @"Check x"); + NSDictionary* dict = @{@"yy": @3, @"yz": @4}; + XCTAssertTrue([[[snap childSnapshotForPath:@"y"] value] isEqualToDictionary:dict], @"Check y"); + + XCTAssertEqualObjects([[[snap childSnapshotForPath:@"y"] childSnapshotForPath:@"yy"] value], @3, @"Check y/yy"); + XCTAssertEqualObjects([[snap childSnapshotForPath:@"y/yz"] value], @4, @"Check y/yz"); + XCTAssertTrue([[snap childSnapshotForPath:@"z"] value] == [NSNull null], @"Check nonexistent z"); + XCTAssertTrue([[snap childSnapshotForPath:@"x/y"] value] == [NSNull null], @"Check value of existent internal node"); + XCTAssertTrue([[[snap childSnapshotForPath:@"x"] childSnapshotForPath:@"y"] value] == [NSNull null], @"Check value of existent internal node"); +} + +- (void) testDataSnapshotHasChildWorks { + FIRDataSnapshot * snap = [self snapshotFor:@{@"x": @5, @"y": @{@"yy": @3, @"yz": @4}}]; + + XCTAssertTrue([snap hasChild:@"x"], @"Has child"); + XCTAssertTrue([snap hasChild:@"y/yy"], @"Has child"); + + XCTAssertFalse([snap hasChild:@"dinosaur dinosaucer"], @"No child"); + XCTAssertFalse([[snap childSnapshotForPath:@"x"] hasChild:@"anything"], @"No child"); + XCTAssertFalse([snap hasChild:@"x/anything/at/all"], @"No child"); +} + +- (void) testDataSnapshotNameWorks { + FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @{@"b": @{@"c": @5}}}]; + + XCTAssertEqualObjects([[snap childSnapshotForPath:@"a"] key], @"a", @"Check child key"); + XCTAssertEqualObjects([[snap childSnapshotForPath:@"a/b/c"] key], @"c", @"Check child key"); + XCTAssertEqualObjects([[snap childSnapshotForPath:@"/a/b/c"] key], @"c", @"Check child key"); + XCTAssertEqualObjects([[snap childSnapshotForPath:@"/a/b/c/"] key], @"c", @"Check child key"); + XCTAssertEqualObjects([[snap childSnapshotForPath:@"////a///b////c///"] key], @"c", @"Check child key"); + XCTAssertEqualObjects([[snap childSnapshotForPath:@"////"] key], [snap key], @"Check root key"); + + XCTAssertEqualObjects([[snap childSnapshotForPath:@"/z/q/r/v////m"] key], @"m", @"Should also work for nonexistent paths"); +} + +- (void) testDataSnapshotForEachWithNoPriorities { + FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @1, @"z": @26, @"m": @13, @"n": @14, @"c": @3, @"b": @2, @"e": @5}]; + + NSMutableString* out = [[NSMutableString alloc] init]; + for (FIRDataSnapshot * child in snap.children) { + [out appendFormat:@"%@:%@:", [child key], [child value] ]; + } + + XCTAssertTrue([out isEqualToString:@"a:1:b:2:c:3:e:5:m:13:n:14:z:26:"], @"Proper order"); +} + +- (void) testDataSnapshotForEachWorksWithNumericPriorities { + FIRDataSnapshot * snap = [self snapshotFor:@{ + @"a": @{@".value" : @1, @".priority": @26}, + @"z": @{@".value" : @26, @".priority": @1}, + @"m": @{@".value" : @13, @".priority": @14}, + @"n": @{@".value" : @14, @".priority": @12}, + @"c": @{@".value" : @3, @".priority": @24}, + @"b": @{@".value" : @2, @".priority": @25}, + @"e": @{@".value" : @5, @".priority": @22}, + }]; + + NSMutableString* out = [[NSMutableString alloc] init]; + for (FIRDataSnapshot * child in snap.children) { + [out appendFormat:@"%@:%@:", [child key], [child value] ]; + } + + XCTAssertTrue([out isEqualToString:@"z:26:n:14:m:13:e:5:c:3:b:2:a:1:"], @"Proper order"); +} + +- (void) testDataSnapshotForEachWorksWithNumericPrioritiesAsStrings { + FIRDataSnapshot * snap = [self snapshotFor:@{ + @"a": @{@".value" : @1, @".priority": @"26"}, + @"z": @{@".value" : @26, @".priority": @"1"}, + @"m": @{@".value" : @13, @".priority": @"14"}, + @"n": @{@".value" : @14, @".priority": @"12"}, + @"c": @{@".value" : @3, @".priority": @"24"}, + @"b": @{@".value" : @2, @".priority": @"25"}, + @"e": @{@".value" : @5, @".priority": @"22"}, + }]; + + NSMutableString* out = [[NSMutableString alloc] init]; + for (FIRDataSnapshot * child in snap.children) { + [out appendFormat:@"%@:%@:", [child key], [child value] ]; + } + + XCTAssertTrue([out isEqualToString:@"z:26:n:14:m:13:e:5:c:3:b:2:a:1:"], @"Proper order"); +} + +- (void) testDataSnapshotForEachWorksAlphaPriorities { + FIRDataSnapshot * snap = [self snapshotFor:@{ + @"a": @{@".value" : @1, @".priority": @"first"}, + @"z": @{@".value" : @26, @".priority": @"second"}, + @"m": @{@".value" : @13, @".priority": @"third"}, + @"n": @{@".value" : @14, @".priority": @"fourth"}, + @"c": @{@".value" : @3, @".priority": @"fifth"}, + @"b": @{@".value" : @2, @".priority": @"sixth"}, + @"e": @{@".value" : @5, @".priority": @"seventh"}, + }]; + + NSMutableString* output = [[NSMutableString alloc] init]; + NSMutableArray* priorities = [[NSMutableArray alloc] init]; + for (FIRDataSnapshot * child in snap.children) { + [output appendFormat:@"%@:%@:", child.key, child.value]; + [priorities addObject:child.priority]; + } + + XCTAssertTrue([output isEqualToString:@"c:3:a:1:n:14:z:26:e:5:b:2:m:13:"], @"Proper order"); + NSArray* expected = @[@"fifth", @"first", @"fourth", @"second", @"seventh", @"sixth", @"third"]; + XCTAssertTrue([priorities isEqualToArray:expected], @"Correct priorities"); + XCTAssertTrue(snap.childrenCount == 7, @"Got correct children count"); +} + + +- (void) testDataSnapshotForEachWorksWithMixedPriorities { + FIRDataSnapshot * snap = [self snapshotFor:@{ + @"alpha42": @{@".value": @1, @".priority": @"zed" }, + @"noPriorityC": @{@".value": @1, @".priority": [NSNull null] }, + @"alpha14": @{@".value": @1, @".priority": @"500" }, + @"noPriorityB": @{@".value": @1, @".priority": [NSNull null] }, + @"num80": @{@".value": @1, @".priority": @4000.1 }, + @"alpha13": @{@".value": @1, @".priority": @"4000" }, + @"alpha11": @{@".value": @1, @".priority": @"24" }, + @"alpha41": @{@".value": @1, @".priority": @"zed" }, + @"alpha20": @{@".value": @1, @".priority": @"horse" }, + @"num20": @{@".value": @1, @".priority": @123 }, + @"num70": @{@".value": @1, @".priority": @4000.01 }, + @"noPriorityA": @{@".value": @1, @".priority": [NSNull null] }, + @"alpha30": @{@".value": @1, @".priority": @"tree" }, + @"alpha12": @{@".value": @1, @".priority": @"300" }, + @"num60": @{@".value": @1, @".priority": @4000.001 }, + @"alpha10": @{@".value": @1, @".priority": @"0horse" }, + @"num42": @{@".value": @1, @".priority": @500 }, + @"alpha40": @{@".value": @1, @".priority": @"zed" }, + @"num40": @{@".value": @1, @".priority": @500 } + }]; + + NSMutableString* out = [[NSMutableString alloc] init]; + for (FIRDataSnapshot * child in snap.children) { + [out appendFormat:@"%@, ", [child key]]; + } + + NSString* expected = @"noPriorityA, noPriorityB, noPriorityC, num20, num40, num42, num60, num70, num80, alpha10, alpha11, alpha12, alpha13, alpha14, alpha20, alpha30, alpha40, alpha41, alpha42, "; + + XCTAssertTrue([expected isEqualToString:out], @"Proper ordering seen"); + +} + +- (void) testIgnoresNullValues { + FIRDataSnapshot * snap = [self snapshotFor:@{@"a": @1, @"b": [NSNull null]}]; + XCTAssertFalse([snap hasChild:@"b"], @"Should not have b, it was null"); +} + +- (void)testNameComparator { + NSComparator keyComparator = [FUtilities keyComparator]; + XCTAssertEqual(keyComparator(@"1234", @"1234"), NSOrderedSame, @"NameComparator compares ints"); + XCTAssertEqual(keyComparator(@"1234", @"12345"), NSOrderedAscending, @"NameComparator compares ints"); + XCTAssertEqual(keyComparator(@"4321", @"1234"), NSOrderedDescending, @"NameComparator compares ints"); + XCTAssertEqual(keyComparator(@"1234", @"zzzz"), NSOrderedAscending, @"NameComparator priorities ints"); + XCTAssertEqual(keyComparator(@"4321", @"12a"), NSOrderedAscending, @"NameComparator priorities ints"); + XCTAssertEqual(keyComparator(@"abc", @"abcd"), NSOrderedAscending, @"NameComparator uses lexiographical sorting for strings."); + XCTAssertEqual(keyComparator(@"zzzz", @"aaaa"), NSOrderedDescending, @"NameComparator compares strings"); + XCTAssertEqual(keyComparator(@"-1234", @"0"), NSOrderedAscending, @"NameComparator compares negative values"); + XCTAssertEqual(keyComparator(@"-1234", @"-1234"), NSOrderedSame, @"NameComparator compares negative values"); + XCTAssertEqual(keyComparator(@"-1234", @"-4321"), NSOrderedDescending, @"NameComparator compares negative values"); + XCTAssertEqual(keyComparator(@"-1234", @"-"), NSOrderedAscending, @"NameComparator does not parse - as integer"); + XCTAssertEqual(keyComparator(@"-", @"1234"), NSOrderedDescending, @"NameComparator does not parse - as integer"); +} + +- (void) testExistsWorks { + FIRDataSnapshot * snap; + + snap = [self snapshotFor:@{}]; + XCTAssertFalse([snap exists], @"Should not exist"); + + snap = [self snapshotFor:@{ @".priority": @"1" }]; + XCTAssertFalse([snap exists], @"Should not exist"); + + snap = [self snapshotFor:[NSNull null]]; + XCTAssertFalse([snap exists], @"Should not exist"); + + snap = [self snapshotFor:[NSNumber numberWithBool:YES]]; + XCTAssertTrue([snap exists], @"Should exist"); + + snap = [self snapshotFor:@5]; + XCTAssertTrue([snap exists], @"Should exist"); + + snap = [self snapshotFor:@{ @"x": @5 }]; + XCTAssertTrue([snap exists], @"Should exist"); +} + +- (void) testUpdatingEmptyChildDoesntOverwriteLeafNode { + FLeafNode *node = [[FLeafNode alloc] initWithValue:@"value"]; + XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@".priority"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node."); + XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@"child"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node."); + XCTAssertEqualObjects(node, [node updateChild:[[FPath alloc] initWith:@"child/.priority"] withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node."); + XCTAssertEqualObjects(node, [node updateImmediateChild:@"child" withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node."); + XCTAssertEqualObjects(node, [node updateImmediateChild:@".priority" withNewChild:[FEmptyNode emptyNode]], @"Update should not affect node."); +} + +/* This was reported by a customer, which broke because 유주연 > 윤규완오빠 but also 윤규완오빠 > 유주연 with the default + * string comparison... */ +- (void)testUnicodeEquality { + FNamedNode *node1 = [[FNamedNode alloc] initWithName:@"a" andNode:[[FLeafNode alloc] initWithValue:@"유주연"]]; + FNamedNode *node2 = [[FNamedNode alloc] initWithName:@"a" andNode:[[FLeafNode alloc] initWithValue:@"윤규완오빠"]]; + id<FIndex> index = [FValueIndex valueIndex]; + + // x < y should imply y > x + XCTAssertEqual([index compareNamedNode:node1 toNamedNode:node2], -[index compareNamedNode:node2 toNamedNode:node1]); +} + +@end diff --git a/Example/Database/Tests/Unit/FIRMutableDataTests.h b/Example/Database/Tests/Unit/FIRMutableDataTests.h new file mode 100644 index 0000000..cd0cec7 --- /dev/null +++ b/Example/Database/Tests/Unit/FIRMutableDataTests.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestBase.h" + +@interface FIRMutableDataTests : FTestBase + +@end diff --git a/Example/Database/Tests/Unit/FIRMutableDataTests.m b/Example/Database/Tests/Unit/FIRMutableDataTests.m new file mode 100644 index 0000000..d36f139 --- /dev/null +++ b/Example/Database/Tests/Unit/FIRMutableDataTests.m @@ -0,0 +1,113 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FIRMutableDataTests.h" +#import "FSnapshotUtilities.h" +#import "FIRMutableData_Private.h" + +@implementation FIRMutableDataTests + +- (FIRMutableData *)dataFor:(id)input { + + id<FNode> node = [FSnapshotUtilities nodeFrom:input]; + return [[FIRMutableData alloc] initWithNode:node]; +} + +- (void) testDataForInWorksAlphaPriorities { + FIRMutableData * data = [self dataFor:@{ + @"a": @{@".value" : @1, @".priority": @"first"}, + @"z": @{@".value" : @26, @".priority": @"second"}, + @"m": @{@".value" : @13, @".priority": @"third"}, + @"n": @{@".value" : @14, @".priority": @"fourth"}, + @"c": @{@".value" : @3, @".priority": @"fifth"}, + @"b": @{@".value" : @2, @".priority": @"sixth"}, + @"e": @{@".value" : @5, @".priority": @"seventh"}, + }]; + + NSMutableString* output = [[NSMutableString alloc] init]; + NSMutableArray* priorities = [[NSMutableArray alloc] init]; + for (FIRMutableData * child in data.children) { + [output appendFormat:@"%@:%@:", child.key, child.value]; + [priorities addObject:child.priority]; + } + + XCTAssertTrue([output isEqualToString:@"c:3:a:1:n:14:z:26:e:5:b:2:m:13:"], @"Proper order"); + NSArray* expected = @[@"fifth", @"first", @"fourth", @"second", @"seventh", @"sixth", @"third"]; + XCTAssertTrue([priorities isEqualToArray:expected], @"Correct priorities"); + XCTAssertTrue(data.childrenCount == 7, @"Got correct children count"); +} + +- (void) testWritingMutableData { + FIRMutableData * data = [self dataFor:@{}]; + + data.value = @{@"a": @1, @"b": @2}; + XCTAssertTrue([data hasChildren], @"Should have children node"); + XCTAssertTrue(data.childrenCount == 2, @"Counts both children"); + XCTAssertTrue([data hasChildAtPath:@"a"], @"Can see the children individually"); + + FIRMutableData * childData = [data childDataByAppendingPath:@"b"]; + XCTAssertTrue([childData.value isEqualToNumber:@2], @"Get the correct child data"); + childData.value = @3; + + NSDictionary* expected = @{@"a": @1, @"b": @3}; + XCTAssertTrue([data.value isEqualToDictionary:expected], @"Updates the parent"); + + int count = 0; + for (FIRDataSnapshot * __unused child in data.children) { + count++; + if (count == 1) { + [data childDataByAppendingPath:@"c"].value = @4; + } + } + XCTAssertTrue(count == 2, @"Should not iterate nodes added while iterating"); + XCTAssertTrue(data.childrenCount == 3, @"Got the new node we added while iterating"); + XCTAssertTrue([[data childDataByAppendingPath:@"c"].value isEqualToNumber:@4], @"Can see the value of the new node"); +} + +- (void) testMutableDataNavigation { + FIRMutableData * data = [self dataFor:@{@"a": @1, @"b": @2}]; + + XCTAssertNil(data.key, @"Root data has no key"); + + // Can get a child + FIRMutableData * childData = [data childDataByAppendingPath:@"b"]; + XCTAssertTrue([childData.key isEqualToString:@"b"], @"Child has correct key"); + + // Can get a non-existent child + childData = [data childDataByAppendingPath:@"c"]; + XCTAssertTrue(childData != nil, @"Wrapper should not be nil"); + XCTAssertTrue([childData.key isEqualToString:@"c"], @"Child should have correct key"); + XCTAssertTrue(childData.value == [NSNull null], @"Non-existent data has no value"); + childData.value = @{@"d": @4}; + + NSDictionary* expected = @{@"a": @1, @"b": @2, @"c": @{@"d": @4}}; + XCTAssertTrue([data.value isEqualToDictionary:expected], @"Setting non-existent child updates parent"); +} + +- (void) testPriorities { + FIRMutableData * data = [self dataFor:@{@"a": @1, @"b": @2}]; + + XCTAssertTrue(data.priority == [NSNull null], @"Should not be a priority"); + data.priority = @"foo"; + XCTAssertTrue([data.priority isEqualToString:@"foo"], @"Should now have a priority"); + data.value = @3; + XCTAssertTrue(data.priority == [NSNull null], @"Setting a value overrides a priority"); + data.priority = @4; + data.value = nil; + XCTAssertTrue(data.priority == [NSNull null], @"Removing the value does remove the priority"); +} + +@end diff --git a/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m new file mode 100644 index 0000000..658a894 --- /dev/null +++ b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m @@ -0,0 +1,583 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +#import "FLevelDBStorageEngine.h" +#import "FSnapshotUtilities.h" +#import "FQueryParams.h" +#import "FPathIndex.h" +#import "FTrackedQuery.h" +#import "FWriteRecord.h" +#import "FTestHelpers.h" +#import "FEmptyNode.h" + +@interface FLevelDBStorageEngineTests : XCTestCase + +@end + +@implementation FLevelDBStorageEngineTests + +- (FLevelDBStorageEngine *)cleanStorageEngine { + NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test-db"]; + FLevelDBStorageEngine *db = [[FLevelDBStorageEngine alloc] initWithPath:path]; + [db purgeEverything]; + return db; +} + +#define SAMPLE_NODE ([FSnapshotUtilities nodeFrom:@{ @"foo": @{ @"bar": @YES, @"baz": @"string" }, @"qux": @2, @"quu": @1.2 }]) + +#define ONE_MEG_NODE ([FTestHelpers leafNodeOfSize:1024*1024]) +#define FIVE_MEG_NODE ([FTestHelpers leafNodeOfSize:5*1024*1024]) +#define TEN_MEG_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024]) +#define TEN_MEG_MINUS_ONE_NODE ([FTestHelpers leafNodeOfSize:10*1024*1024 - 1]) + +#define SAMPLE_PARAMS \ + ([[[[[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:PATH(@"child")]] \ + startAt:[FSnapshotUtilities nodeFrom:@"startVal"] childKey:@"startKey"] \ + endAt:[FSnapshotUtilities nodeFrom:@"endVal"] childKey:@"endKey"] \ + limitToLast:5]) + +#define SAMPLE_QUERY \ + ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:SAMPLE_PARAMS]) + +#define DEFAULT_FOO_QUERY \ + ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:[FQueryParams defaultInstance]]) + +#define SAMPLE_TRACKED_QUERY \ + ([[FTrackedQuery alloc] initWithId:1 \ + query:SAMPLE_QUERY \ + isPinned:NO \ + lastUse:100 \ + Active:NO \ + isComplete:NO]) +#define OVERWRITE_RECORD(__path, __node, __writeId) \ + ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] overwrite:__node writeId:__writeId visible:YES]) + +#define MERGE_RECORD(__path, __merge, __writeId) \ + ([[FWriteRecord alloc] initWithPath:[FPath pathWithString:__path] merge:__merge writeId:__writeId]) + +- (void)testUserWriteIsPersisted { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:SAMPLE_NODE atPath:[FPath pathWithString:@"foo/bar"] writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"foo/bar", SAMPLE_NODE, 1)]); +} + +- (void)testUserMergeIsPersisted { + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @{@"bar": @1, @"baz": @"string"}, @"quu": @YES}]; + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]); +} + +- (void)testDeepUserMergeIsPersisted { + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @1, @"foo/baz": @"string", @"quu/qux": @YES, @"shallow": @2}]; + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[MERGE_RECORD(@"foo/bar", merge, 1)]); +} + +- (void)testSameWriteIdOverwritesOldWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:NODE(@"first") atPath:PATH(@"foo/bar") writeId:1]; + [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1]; + XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]); +} + +- (void)testHugeWriteWorks { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1]; + FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:2]; + NSArray *expected = @[OVERWRITE_RECORD(@"foo/bar", TEN_MEG_NODE, 1), MERGE_RECORD(@"foo/bar", merge, 2)]; + XCTAssertEqualObjects(engine.userWrites, expected); +} + +- (void)testHugeWritesCanBeDeleted { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1]; + [engine removeUserWrite:1]; + XCTAssertTrue(engine.userWrites.count == 0); +} + +- (void)testHugeWritesCanBeInterleavedWithSmallWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2]; + [engine saveUserOverwrite:NODE(@"node-3") atPath:PATH(@"foo/3") writeId:3]; + [engine saveUserOverwrite:FIVE_MEG_NODE atPath:PATH(@"foo/4") writeId:4]; + + NSArray *expected = @[OVERWRITE_RECORD(@"foo/1", NODE(@"node-1"), 1), + OVERWRITE_RECORD(@"foo/2", TEN_MEG_NODE, 2), + OVERWRITE_RECORD(@"foo/3", NODE(@"node-3"), 3), + OVERWRITE_RECORD(@"foo/4", FIVE_MEG_NODE, 4)]; + XCTAssertEqualObjects(engine.userWrites, expected); +} + +// This is ported from the Android client and doesn't really make sense since we don't have multi part writes, but +// It's always good to have tests, so what the heck... +- (void)testSameWriteIdOverwritesOldMultiPartWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/bar") writeId:1]; + [engine saveUserOverwrite:NODE(@"second") atPath:PATH(@"other/path") writeId:1]; + + XCTAssertEqualObjects(engine.userWrites, @[OVERWRITE_RECORD(@"other/path", NODE(@"second"), 1)]); +} + +- (void)testWritesAreReturnedInOrder { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + NSUInteger count = 20; + for (NSUInteger i = count - 1; i > 0; i--) { + NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i]; + [engine saveUserOverwrite:NODE(@(i)) atPath:PATH(path) writeId:i]; + } + NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)count]; + [engine saveUserOverwrite:NODE(@(count)) atPath:PATH(path) writeId:count]; + NSArray *userWrites = engine.userWrites; + XCTAssertEqual(userWrites.count, count); + for (NSUInteger i = 1; i <= count; i++) { + NSString *path = [NSString stringWithFormat:@"foo/%lu", (unsigned long)i]; + XCTAssertEqualObjects(userWrites[i-1], OVERWRITE_RECORD(path, NODE(@(i)), i)); + } +} + +- (void)testRemoveAllUserWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine saveUserOverwrite:NODE(@"node-1") atPath:PATH(@"foo/1") writeId:1]; + [engine saveUserOverwrite:TEN_MEG_NODE atPath:PATH(@"foo/2") writeId:2]; + FCompoundWrite *merge = [[FCompoundWrite emptyWrite] addWrite:TEN_MEG_NODE atKey:@"update"]; + [engine saveUserMerge:merge atPath:PATH(@"foo/bar") writeId:3]; + [engine removeAllUserWrites]; + XCTAssertEqualObjects(engine.userWrites, @[]); +} + + +- (void)testCacheSavedIsReturned { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], SAMPLE_NODE); +} + +- (void)testCacheSavedIsReturnedAtRoot { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], SAMPLE_NODE); +} + +- (void)testLaterCacheWritesOverwriteOlderWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO]; + // this does not affect the node + [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO]; + [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO]; + [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO]; + + id<FNode> expected = [[SAMPLE_NODE updateImmediateChild:@"bar" withNewChild:NODE(@"latest-bar")] + updateImmediateChild:@"later-qux" withNewChild:NODE(@"later-qux")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testLaterCacheWritesOverwriteOlderDeeperWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"later-bar") atPath:PATH(@"foo/bar") merge:NO]; + // this does not affect the node + [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO]; + [engine updateServerCache:NODE(@"later-qux") atPath:PATH(@"foo/later-qux") merge:NO]; + [engine updateServerCache:NODE(@"latest-bar") atPath:PATH(@"foo/bar") merge:NO]; + [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"latest-foo")); +} + +- (void)testLaterCacheWritesDontAffectEarlierWritesAtUnaffectedPath { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:SAMPLE_NODE atPath:PATH(@"foo") merge:NO]; + // this does not affect the node + [engine updateServerCache:NODE(@"unaffected") atPath:PATH(@"unaffected") merge:NO]; + [engine updateServerCache:NODE(@"latest-foo") atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"unaffected")], NODE(@"unaffected")); +} + +- (void)testMergeOnEmptyCacheGivesResults { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + NSDictionary *mergeData = @{@"foo": @"foo-value", @"bar": @"bar-value"}; + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:mergeData]; + [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(mergeData)); +} + +- (void)testMergePartlyOverwritingPreviousWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> existingNode = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO]; + + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}]; + [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")]; + + id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testDeepMergePartlyOverwritingPreviousWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> existingNode = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value"})); + [engine updateServerCache:existingNode atPath:PATH(@"foo") merge:NO]; + + FCompoundWrite *merge = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo/bar": @"new-bar-value", @"quu": @"quu-value"}]; + [engine updateServerCacheWithMerge:merge atPath:PATH(@"foo")]; + + id<FNode> expected = NODE((@{@"foo": @{ @"bar": @"new-bar-value", @"baz": @"baz-value"}, @"qux": @"qux-value", @"quu": @"quu-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testMergePartlyOverwritingPreviousMerge { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + FCompoundWrite *merge1 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"foo-value", @"bar": @"bar-value"}]; + [engine updateServerCacheWithMerge:merge1 atPath:PATH(@"foo")]; + + FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}]; + [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")]; + + id<FNode> expected = NODE((@{@"foo": @"new-foo-value", @"bar": @"bar-value", @"baz": @"baz-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testOverwriteRemovesPreviousMerge { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO]; + + FCompoundWrite *merge2 = [FCompoundWrite compoundWriteWithValueDictionary:@{@"foo": @"new-foo-value", @"baz": @"baz-value"}]; + [engine updateServerCacheWithMerge:merge2 atPath:PATH(@"foo")]; + + id<FNode> replacingNode = NODE((@{@"qux": @"qux-value", @"quu": @"quu-value"})); + [engine updateServerCache:replacingNode atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], replacingNode); +} + +- (void)testEmptyOverwriteDeletesNodeFromHigherWrite { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO]; + + // delete bar + [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO]; + + id<FNode> expected = NODE((@{@"foo": @"foo-value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], expected); +} + +- (void)testDeeperReadFromHigherSet { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + id<FNode> initial = NODE((@{@"foo": @"foo-value", @"bar": @"bar-value"})); + [engine updateServerCache:initial atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/bar")], NODE(@"bar-value")); +} + +- (void)testDeeperLeafNodeSetRemovesHigherLeafNodes { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:NODE(@"level-0") atPath:PATH(@"") merge:NO]; + [engine updateServerCache:NODE(@"level-1") atPath:PATH(@"lvl1") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], NODE((@{@"lvl1": @"level-1"}))); + + [engine updateServerCache:NODE(@"level-2") atPath:PATH(@"lvl1/lvl2") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @"level-2"}))); + + [engine updateServerCache:NODE(@"level-4") atPath:PATH(@"lvl1/lvl2/lvl3/lvl4") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"lvl1")], NODE((@{@"lvl2": @{@"lvl3": @{@"lvl4": @"level-4"}}}))); +} + + +// This test causes a split on Android so it doesn't really make sense here, but why not test anyways... +- (void)testHugeNodeWithSplit { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + id<FNode> outer = [FEmptyNode emptyNode]; + // This structure ensures splits at various depths + for (NSUInteger i = 0; i < 100; i++) { // Outer + id<FNode> inner = [FEmptyNode emptyNode]; + for (NSUInteger j = 0; j < i; j++) { // Inner + id<FNode> innerMost = [FEmptyNode emptyNode]; + for (NSUInteger k = 0; k < j; k++) { + NSString *key = [NSString stringWithFormat:@"key-%lu", (unsigned long)k]; + id<FNode> node = NODE(([NSString stringWithFormat:@"leaf-%lu", (unsigned long)k])); + innerMost = [innerMost updateImmediateChild:key withNewChild:node]; + } + NSString *innerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)j]; + inner = [inner updateImmediateChild:innerKey withNewChild:innerMost]; + } + NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i]; + outer = [outer updateImmediateChild:outerKey withNewChild:inner]; + } + [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer); +} + +- (void)testManyLargeLeafNodes { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> outer = [FEmptyNode emptyNode]; + for (NSUInteger i = 0; i < 30; i++) { + NSString *outerKey = [NSString stringWithFormat:@"key-%lu", (unsigned long)i]; + outer = [outer updateImmediateChild:outerKey withNewChild:ONE_MEG_NODE]; + } + + [engine updateServerCache:outer atPath:PATH(@"foo") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], outer); +} + +- (void)testPriorityWorks { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine updateServerCache:NODE(@"bar-value") atPath:PATH(@"foo/bar") merge:NO]; + [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{ @".priority": @"prio-value", @"bar": @"bar-value"}))); +} + +- (void)testSimilarSiblingsAreNotLoaded { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine updateServerCache:NODE(@"value") atPath:PATH(@"foo/123") merge:NO]; + [engine updateServerCache:NODE(@"sibling-value") atPath:PATH(@"foo/1230") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/123")], NODE(@"value")); +} + +// TODO: this test fails, but it is a rare edge case around priorities which would require a bunch of code +// Fix whenever we have too much time on our hands +- (void)priorityIsCleared { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + [engine updateServerCache:NODE((@{@"bar": @"bar-value"})) atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"prio-value") atPath:PATH(@"foo/.priority") merge:NO]; + [engine updateServerCache:NODE(nil) atPath:PATH(@"foo/bar") merge:NO]; + [engine updateServerCache:NODE(@"baz-value") atPath:PATH(@"foo/baz") merge:NO]; + + // Priority should have been cleaned out + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"baz": @"baz-value"})); +} + +- (void)testHugeLeafNode { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], TEN_MEG_NODE); +} + +- (void)testHugeLeafNodeSiblings { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo/one") merge:NO]; + [engine updateServerCache:TEN_MEG_MINUS_ONE_NODE atPath:PATH(@"foo/two") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/one")], TEN_MEG_NODE); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/two")], TEN_MEG_MINUS_ONE_NODE); +} + +- (void)testHugeLeafNodeThenTinyLeafNode { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"tiny") atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@"tiny")); +} + +- (void)testHugeLeafNodeThenSmallerLeafNode { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:FIVE_MEG_NODE atPath:PATH(@"foo") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], FIVE_MEG_NODE); +} + +- (void)testHugeLeafNodeThenDeeperSet { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:TEN_MEG_NODE atPath:PATH(@"foo") merge:NO]; + [engine updateServerCache:NODE(@"deep-value") atPath:PATH(@"foo/deep") merge:NO]; + + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE((@{@"deep": @"deep-value"}))); +} + +// Well this is awkward, but NSJSONSerialization fails to deserialize JSON with tiny/huge doubles +// It is kind of bad we raise "invalid" data, but at least we don't crash *trollface* +- (void)testExtremeDoublesAsServerCache { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:NODE((@{@"works": @"value", @"fails": @(2.225073858507201e-308)})) atPath:PATH(@"foo") merge:NO]; + + // Will drop the tiny double + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo")], NODE(@{@"works": @"value"})); + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"foo/fails")], [FEmptyNode emptyNode]); +} + +- (void)testExtremeDoublesAsTrackedQuery { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> tinyDouble = NODE(@(2.225073858507201e-308)); + + FQueryParams *params = [[[FQueryParams defaultInstance] startAt:tinyDouble] endAt:tinyDouble]; + FTrackedQuery *doesNotWork = [[FTrackedQuery alloc] initWithId:0 + query:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:params] + lastUse:0 + isActive:NO]; + FTrackedQuery *doesWork = [[FTrackedQuery alloc] initWithId:1 + query:[FQuerySpec defaultQueryAtPath:PATH(@"bar")] + lastUse:0 + isActive:NO]; + [engine saveTrackedQuery:doesNotWork]; + [engine saveTrackedQuery:doesWork]; + // One will be dropped, the other should still be there + XCTAssertEqualObjects([engine loadTrackedQueries], @[doesWork]); +} + +- (void)testExtremeDoublesAsUserWrites { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + id<FNode> tinyDouble = NODE(@(2.225073858507201e-308)); + + [engine saveUserOverwrite:tinyDouble atPath:PATH(@"foo") writeId:1]; + [engine saveUserMerge:[[FCompoundWrite emptyWrite] addWrite:tinyDouble atPath:PATH(@"bar")] atPath:PATH(@"foo") writeId:2]; + [engine saveUserOverwrite:NODE(@"should-work") atPath:PATH(@"other") writeId:3]; + + // The other two should be dropped and only the valid should remain + XCTAssertEqualObjects([engine userWrites], @[[[FWriteRecord alloc] initWithPath:PATH(@"other") + overwrite:NODE(@"should-work") + writeId:3 + visible:YES]]); +} + +- (void)testLongValuesDontLosePrecision { + id longValue = @1542405709418655810; + id floatValue = @2.47; + id<FNode> expectedData = NODE((@{@"long": longValue, @"float": floatValue})); + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:expectedData atPath:PATH(@"foo") merge:NO]; + id<FNode> actualData = [engine serverCacheAtPath:PATH(@"foo")]; + NSDictionary* value = [actualData val]; + XCTAssertEqualObjects([value[@"long"] stringValue], [longValue stringValue]); + XCTAssertEqualObjects([value[@"float"] stringValue], [floatValue stringValue]); +} + +// NSJSONSerialization has a bug in which it rounds doubles wrongly so hashes end up not matching on the server for +// some doubles (including 2.47). Make sure LevelDB has the correct hash for that +- (void)testDoublesAreRoundedProperly { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine updateServerCache:NODE(@(2.47)) atPath:PATH(@"foo") merge:NO]; + + // Expected hash for 2.47 parsed correctly + NSString *hashFor247 = @"EsibHXKcBp2/b/bn/a0C5WffcUU="; + XCTAssertEqualObjects([[engine serverCacheAtPath:PATH(@"foo")] dataHash], hashFor247); +} + +// TODO[offline]: Somehow test estimated server size? +// TODO[offline]: Test pruning! + +- (void)testSaveAndLoadTrackedQueries { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + NSArray *queries = @[[[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO], + [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:200 isActive:NO isComplete:NO], + [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:300 isActive:YES isComplete:NO], + [[FTrackedQuery alloc] initWithId:4 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:400 isActive:NO isComplete:YES], + [[FTrackedQuery alloc] initWithId:5 query:[FQuerySpec defaultQueryAtPath:PATH(@"foo")] lastUse:500 isActive:NO isComplete:NO]]; + + [queries enumerateObjectsUsingBlock:^(FTrackedQuery *query, NSUInteger idx, BOOL *stop) { + [engine saveTrackedQuery:query]; + }]; + + XCTAssertEqualObjects([engine loadTrackedQueries], queries); +} + +- (void)testOverwriteTrackedQueryById { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + + FTrackedQuery *first = [[FTrackedQuery alloc] initWithId:1 query:SAMPLE_QUERY lastUse:100 isActive:NO isComplete:NO]; + FTrackedQuery *second = [[FTrackedQuery alloc] initWithId:1 query:DEFAULT_FOO_QUERY lastUse:200 isActive:YES isComplete:YES]; + [engine saveTrackedQuery:first]; + [engine saveTrackedQuery:second]; + + XCTAssertEqualObjects([engine loadTrackedQueries], @[second]); +} + +- (void)testDeleteTrackedQuery { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO]; + FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:YES isComplete:NO]; + FTrackedQuery *query3 = [[FTrackedQuery alloc] initWithId:3 query:[FQuerySpec defaultQueryAtPath:PATH(@"c")] lastUse:300 isActive:NO isComplete:YES]; + [engine saveTrackedQuery:query1]; + [engine saveTrackedQuery:query2]; + [engine saveTrackedQuery:query3]; + + [engine removeTrackedQuery:2]; + XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query3])); +} + +- (void)testSaveAndLoadTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + NSSet *keys = [NSSet setWithArray:@[@"foo", @"☁", @"10", @"٩(͡๏̯͡๏)۶"]]; + [engine setTrackedQueryKeys:keys forQueryId:1]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"not", @"included"]] forQueryId:2]; + + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], keys); +} + +- (void)testSaveOverwritesTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]] forQueryId:1]; + + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]])); +} + +- (void)testUpdateTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b", @"c"]] forQueryId:1]; + [engine updateTrackedQueryKeysWithAddedKeys:[NSSet setWithArray:@[@"c", @"d", @"e"]] + removedKeys:[NSSet setWithArray:@[@"a", @"b"]] + forQueryId:1]; + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"c", @"d", @"e"]])); +} + +- (void)testRemoveTrackedQueryRemovesTrackedQueryKeys { + FLevelDBStorageEngine *engine = [self cleanStorageEngine]; + FTrackedQuery *query1 = [[FTrackedQuery alloc] initWithId:1 query:[FQuerySpec defaultQueryAtPath:PATH(@"a")] lastUse:100 isActive:NO isComplete:NO]; + FTrackedQuery *query2 = [[FTrackedQuery alloc] initWithId:2 query:[FQuerySpec defaultQueryAtPath:PATH(@"b")] lastUse:200 isActive:NO isComplete:NO]; + [engine saveTrackedQuery:query1]; + [engine saveTrackedQuery:query2]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b"]] forQueryId:1]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"b", @"c"]] forQueryId:2]; + + XCTAssertEqualObjects([engine loadTrackedQueries], (@[query1, query2])); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], ([NSSet setWithArray:@[@"a", @"b"]])); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]])); + + [engine removeTrackedQuery:1]; + + XCTAssertEqualObjects([engine loadTrackedQueries], (@[query2])); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:1], [NSSet set]); + XCTAssertEqualObjects([engine trackedQueryKeysForQuery:2], ([NSSet setWithArray:@[@"b", @"c"]])); +} + +@end diff --git a/Example/Database/Tests/Unit/FNodeTests.m b/Example/Database/Tests/Unit/FNodeTests.m new file mode 100644 index 0000000..372b84f --- /dev/null +++ b/Example/Database/Tests/Unit/FNodeTests.m @@ -0,0 +1,174 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> + +#import "FSnapshotUtilities.h" +#import "FEmptyNode.h" +#import "FChildrenNode.h" +#import "FLeafNode.h" + +@interface FNodeTests : XCTestCase + +@end + +@implementation FNodeTests + +- (void) testLeafNodeEqualsHashCode { + id<FNode> falseNode = [FSnapshotUtilities nodeFrom:@NO]; + id<FNode> trueNode = [FSnapshotUtilities nodeFrom:@YES]; + id<FNode> stringOneNode = [FSnapshotUtilities nodeFrom:@"one"]; + id<FNode> stringTwoNode = [FSnapshotUtilities nodeFrom:@"two"]; + id<FNode> zeroNode = [FSnapshotUtilities nodeFrom:@0]; + id<FNode> oneNode = [FSnapshotUtilities nodeFrom:@1]; + id<FNode> emptyNode1 = [FSnapshotUtilities nodeFrom:nil]; + id<FNode> emptyNode2 = [FSnapshotUtilities nodeFrom:[NSNull null]]; + + XCTAssertEqualObjects(falseNode, [FSnapshotUtilities nodeFrom:@NO]); + XCTAssertEqual(falseNode.hash, [FSnapshotUtilities nodeFrom:@NO].hash); + XCTAssertEqualObjects(trueNode, [FSnapshotUtilities nodeFrom:@YES]); + XCTAssertEqual(trueNode.hash, [FSnapshotUtilities nodeFrom:@YES].hash); + XCTAssertFalse([falseNode isEqual:trueNode]); + XCTAssertFalse([falseNode isEqual:oneNode]); + XCTAssertFalse([falseNode isEqual:stringOneNode]); + XCTAssertFalse([falseNode isEqual:emptyNode1]); + + XCTAssertEqualObjects(stringOneNode, [FSnapshotUtilities nodeFrom:@"one"]); + XCTAssertEqual(stringOneNode.hash, [FSnapshotUtilities nodeFrom:@"one"].hash); + XCTAssertFalse([stringOneNode isEqual:stringTwoNode]); + XCTAssertFalse([stringOneNode isEqual:emptyNode1]); + XCTAssertFalse([stringOneNode isEqual:oneNode]); + XCTAssertFalse([stringOneNode isEqual:trueNode]); + + XCTAssertEqualObjects(zeroNode, [FSnapshotUtilities nodeFrom:@0]); + XCTAssertEqual(zeroNode.hash, [FSnapshotUtilities nodeFrom:@0].hash); + XCTAssertFalse([zeroNode isEqual:oneNode]); + XCTAssertFalse([zeroNode isEqual:emptyNode1]); + XCTAssertFalse([zeroNode isEqual:falseNode]); + + XCTAssertEqualObjects(emptyNode1, emptyNode2); + XCTAssertEqual(emptyNode1.hash, emptyNode2.hash); +} + +- (void) testLeafNodePrioritiesEqualsHashCode { + id<FNode> oneOne = [FSnapshotUtilities nodeFrom:@1 priority:@1]; + id<FNode> stringOne = [FSnapshotUtilities nodeFrom:@"value" priority:@1]; + id<FNode> oneString = [FSnapshotUtilities nodeFrom:@1 priority:@"value"]; + id<FNode> stringString = [FSnapshotUtilities nodeFrom:@"value" priority:@"value"]; + + XCTAssertEqualObjects(oneOne, [FSnapshotUtilities nodeFrom:@1 priority:@1]); + XCTAssertEqual(oneOne.hash, [FSnapshotUtilities nodeFrom:@1 priority:@1].hash); + XCTAssertFalse([oneOne isEqual:stringOne]); + XCTAssertFalse([oneOne isEqual:oneString]); + XCTAssertFalse([oneOne isEqual:stringString]); + + XCTAssertEqualObjects(stringOne, [FSnapshotUtilities nodeFrom:@"value" priority:@1]); + XCTAssertEqual(stringOne.hash, [FSnapshotUtilities nodeFrom:@"value" priority:@1].hash); + XCTAssertFalse([stringOne isEqual:oneOne]); + XCTAssertFalse([stringOne isEqual:oneString]); + XCTAssertFalse([stringOne isEqual:stringString]); + + XCTAssertEqualObjects(oneString, [FSnapshotUtilities nodeFrom:@1 priority:@"value"]); + XCTAssertEqual(oneString.hash, [FSnapshotUtilities nodeFrom:@1 priority:@"value"].hash); + XCTAssertFalse([oneString isEqual:stringOne]); + XCTAssertFalse([oneString isEqual:oneOne]); + XCTAssertFalse([oneString isEqual:stringString]); + + XCTAssertEqualObjects(stringString, [FSnapshotUtilities nodeFrom:@"value" priority:@"value"]); + XCTAssertEqual(stringString.hash, [FSnapshotUtilities nodeFrom:@"value" priority:@"value"].hash); + XCTAssertFalse([stringString isEqual:stringOne]); + XCTAssertFalse([stringString isEqual:oneString]); + XCTAssertFalse([stringString isEqual:oneOne]); +} + +- (void)testChildrenNodeEqualsHashCode { + id<FNode> nodeOne = [FSnapshotUtilities nodeFrom:@{ @"one": @1, @"two": @2, @".priority": @"prio"}]; + id<FNode> nodeTwo = [[FEmptyNode emptyNode] updateImmediateChild:@"one" withNewChild:[FSnapshotUtilities nodeFrom:@1]]; + nodeTwo = [nodeTwo updateImmediateChild:@"two" withNewChild:[FSnapshotUtilities nodeFrom:@2]]; + nodeTwo = [nodeTwo updatePriority:[FSnapshotUtilities nodeFrom:@"prio"]]; + + XCTAssertEqualObjects(nodeOne, nodeTwo); + XCTAssertEqual(nodeOne.hash, nodeTwo.hash); + XCTAssertFalse([[nodeOne updatePriority:[FEmptyNode emptyNode]] isEqual:nodeOne]); + XCTAssertFalse([[nodeOne updateImmediateChild:@"one" withNewChild:[FEmptyNode emptyNode]] isEqual:nodeOne]); + XCTAssertFalse([[nodeOne updateImmediateChild:@"one" withNewChild:[FSnapshotUtilities nodeFrom:@2]] isEqual:nodeOne]); +} + +- (void)testLeadingZerosWorkCorrectly { + NSDictionary *data = @{ @"1": @1, @"01": @2, @"001": @3, @"0001": @4 }; + + id<FNode> node = [FSnapshotUtilities nodeFrom:data]; + XCTAssertEqualObjects([node getImmediateChild:@"1"].val, @1); + XCTAssertEqualObjects([node getImmediateChild:@"01"].val, @2); + XCTAssertEqualObjects([node getImmediateChild:@"001"].val, @3); + XCTAssertEqualObjects([node getImmediateChild:@"0001"].val, @4); +} + +- (void)testLeadindZerosArePreservedInValue { + NSDictionary *data = @{ @"1": @1, @"01": @2, @"001": @3, @"0001": @4 }; + + XCTAssertEqualObjects([FSnapshotUtilities nodeFrom:data].val, data); +} + +- (void)testEmptyNodeEqualsEmptyChildrenNode { + XCTAssertEqualObjects([FEmptyNode emptyNode], [[FChildrenNode alloc] init]); + XCTAssertEqualObjects([[FChildrenNode alloc] init], [FEmptyNode emptyNode]); + XCTAssertEqual([[FChildrenNode alloc] init].hash, [FEmptyNode emptyNode].hash); +} + +- (void)testUpdatingEmptyChildrenDoesntOverwriteLeafNode { + FLeafNode *node = [[FLeafNode alloc] initWithValue:@"value"]; + XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@".priority"] withNewChild:[FEmptyNode emptyNode]]); + XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@"child"] withNewChild:[FEmptyNode emptyNode]]); + XCTAssertEqualObjects(node, [node updateChild:[FPath pathWithString:@"child/.priority"] withNewChild:[FEmptyNode emptyNode]]); + XCTAssertEqualObjects(node, [node updateImmediateChild:@"child" withNewChild:[FEmptyNode emptyNode]]); + XCTAssertEqualObjects(node, [node updateImmediateChild:@".priority" withNewChild:[FEmptyNode emptyNode]]); +} + +- (void)testUpdatingPrioritiesOnEmptyNodesIsANoOp { + id<FNode> priority = [FSnapshotUtilities nodeFrom:@"prio"]; + XCTAssertTrue([[[[FEmptyNode emptyNode] updatePriority:priority] getPriority] isEmpty]); + XCTAssertTrue([[[[FEmptyNode emptyNode] updateChild:[FPath pathWithString:@".priority"] withNewChild:priority] getPriority] isEmpty]); + XCTAssertTrue([[[[FEmptyNode emptyNode] updateImmediateChild:@".priority" withNewChild:priority] getPriority] isEmpty]); + + id<FNode> valueNode = [FSnapshotUtilities nodeFrom:@"value"]; + FPath *childPath = [FPath pathWithString:@"child"]; + id<FNode> reemptiedChildren = [[[FEmptyNode emptyNode] updateChild:childPath withNewChild:valueNode] updateChild:childPath withNewChild:[FEmptyNode emptyNode]]; + XCTAssertTrue([[[reemptiedChildren updatePriority:priority] getPriority] isEmpty]); + XCTAssertTrue([[[reemptiedChildren updateChild:[FPath pathWithString:@".priority"] withNewChild:priority] getPriority] isEmpty]); + XCTAssertTrue([[[reemptiedChildren updateImmediateChild:@".priority" withNewChild:priority] getPriority] isEmpty]); +} + +- (void)testDeletingLastChildFromChildrenNodeRemovesPriority { + id<FNode> priority = [FSnapshotUtilities nodeFrom:@"prio"]; + id<FNode> valueNode = [FSnapshotUtilities nodeFrom:@"value"]; + FPath *childPath = [FPath pathWithString:@"child"]; + id<FNode> withPriority = [[[FEmptyNode emptyNode] updateChild:childPath withNewChild:valueNode] updatePriority:priority]; + XCTAssertEqualObjects(priority, [withPriority getPriority]); + id<FNode> deletedChild = [withPriority updateChild:childPath withNewChild:[FEmptyNode emptyNode]]; + XCTAssertTrue([[deletedChild getPriority] isEmpty]); +} + +- (void)testFromNodeReturnsEmptyNodesWithoutPriority { + id<FNode> empty1 = [FSnapshotUtilities nodeFrom:@{ @".priority": @"prio" }]; + XCTAssertTrue([[empty1 getPriority] isEmpty]); + + id<FNode> empty2 = [FSnapshotUtilities nodeFrom:@{ @"dummy": [NSNull null], @".priority": @"prio" }]; + XCTAssertTrue([[empty2 getPriority] isEmpty]); +} + +@end diff --git a/Example/Database/Tests/Unit/FPathTests.h b/Example/Database/Tests/Unit/FPathTests.h new file mode 100644 index 0000000..edd8330 --- /dev/null +++ b/Example/Database/Tests/Unit/FPathTests.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestBase.h" + +@interface FPathTests : FTestBase + +@end diff --git a/Example/Database/Tests/Unit/FPathTests.m b/Example/Database/Tests/Unit/FPathTests.m new file mode 100644 index 0000000..9b26a85 --- /dev/null +++ b/Example/Database/Tests/Unit/FPathTests.m @@ -0,0 +1,84 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FPathTests.h" +#import "FPath.h" + +@implementation FPathTests + +- (void)testContains +{ + XCTAssertTrue([[[FPath alloc] initWith:@"/"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct"); + XCTAssertTrue([[[FPath alloc] initWith:@"/a"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct"); + XCTAssertTrue([[[FPath alloc] initWith:@"/a/b"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct"); + XCTAssertTrue([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct"); + + XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/b"]], @"contains should be correct"); + XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a"]], @"contains should be correct"); + XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/"]], @"contains should be correct"); + + NSArray *pathPieces = @[@"a",@"b",@"c"]; + + XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct"); + XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c/d"]], @"contains should be correct"); + + XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct"); + XCTAssertFalse([[[FPath alloc] initWith:@"/a/b/c"] contains:[[FPath alloc] initWith:@"/a/c/b"]], @"contains should be correct"); + + XCTAssertFalse([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1]contains:[[FPath alloc] initWith:@"/a/b/c"]], @"contains should be correct"); + XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c"]], @"contains should be correct"); + XCTAssertTrue([[[FPath alloc] initWithPieces:pathPieces andPieceNum:1] contains:[[FPath alloc] initWith:@"/b/c/d"]], @"contains should be correct"); +} + +- (void)testPopFront +{ + XCTAssertEqualObjects([[[FPath alloc] initWith:@"/a/b/c"] popFront], [[FPath alloc] initWith:@"/b/c"], @"should be correct"); + XCTAssertEqualObjects([[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront], [[FPath alloc] initWith:@"/c"], @"should be correct"); + XCTAssertEqualObjects([[[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront] popFront], [[FPath alloc] initWith:@"/"], @"should be correct"); + XCTAssertEqualObjects([[[[[[FPath alloc] initWith:@"/a/b/c"] popFront] popFront] popFront] popFront], [[FPath alloc] initWith:@"/"], @"should be correct"); +} + +- (void)testParent +{ + XCTAssertEqualObjects([[[FPath alloc] initWith:@"/a/b/c"] parent], [[FPath alloc] initWith:@"/a/b/"], @"should be correct"); + XCTAssertEqualObjects([[[[FPath alloc] initWith:@"/a/b/c"] parent] parent], [[FPath alloc] initWith:@"/a/"], @"should be correct"); + XCTAssertEqualObjects([[[[[FPath alloc] initWith:@"/a/b/c"] parent] parent] parent], [[FPath alloc] initWith:@"/"], @"should be correct"); + XCTAssertNil([[[[[[FPath alloc] initWith:@"/a/b/c"] parent] parent] parent] parent], @"should be correct"); +} + +- (void)testWireFormat +{ + XCTAssertEqualObjects(@"/", [[FPath empty] wireFormat]); + XCTAssertEqualObjects(@"a/b/c", [[[FPath alloc] initWith:@"/a/b//c/"] wireFormat]); + XCTAssertEqualObjects(@"b/c", [[[[FPath alloc] initWith:@"/a/b//c/"] popFront] wireFormat]); +} + +- (void)testComparison +{ + NSArray *pathsInOrder = @[@"1", @"2", @"10", @"a", @"a/1", @"a/2", @"a/10", @"a/a", @"a/aa", @"a/b", @"a/b/c", + @"b", @"b/a"]; + for (NSInteger i = 0; i < pathsInOrder.count; i++) { + FPath *path1 = PATH(pathsInOrder[i]); + for (NSInteger j = i + 1; j < pathsInOrder.count; j++) { + FPath *path2 = PATH(pathsInOrder[j]); + XCTAssertEqual([path1 compare:path2], NSOrderedAscending); + XCTAssertEqual([path2 compare:path1], NSOrderedDescending); + } + XCTAssertEqual([path1 compare:path1], NSOrderedSame); + } +} + +@end diff --git a/Example/Database/Tests/Unit/FPersistenceManagerTest.m b/Example/Database/Tests/Unit/FPersistenceManagerTest.m new file mode 100644 index 0000000..c00d11f --- /dev/null +++ b/Example/Database/Tests/Unit/FPersistenceManagerTest.m @@ -0,0 +1,106 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> +#import "FPersistenceManager.h" +#import "FTestCachePolicy.h" +#import "FMockStorageEngine.h" +#import "FTestHelpers.h" +#import "FQuerySpec.h" +#import "FSnapshotUtilities.h" +#import "FPathIndex.h" +#import "FIndexedNode.h" +#import "FEmptyNode.h" + +@interface FPersistenceManagerTest : XCTestCase + +@end + +@implementation FPersistenceManagerTest + +- (FPersistenceManager *)newTestPersistenceManager { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FPersistenceManager *manager = [[FPersistenceManager alloc] initWithStorageEngine:engine + cachePolicy:[FNoCachePolicy noCachePolicy]]; + return manager; +} + +- (void)testServerCacheFiltersResults1 { + FPersistenceManager *manager = [self newTestPersistenceManager]; + + [manager updateServerCacheWithNode:NODE(@"1") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]]; + [manager updateServerCacheWithNode:NODE(@"2") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/baz")]]; + [manager updateServerCacheWithNode:NODE(@"3") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/quu/1")]]; + [manager updateServerCacheWithNode:NODE(@"4") forQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/quu/2")]]; + + FCacheNode *cache = [manager serverCacheForQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]]; + XCTAssertFalse(cache.isFullyInitialized); + XCTAssertEqualObjects(cache.node, [FEmptyNode emptyNode]); +} + +- (void)testServerCacheFiltersResults2 { + FPersistenceManager *manager = [self newTestPersistenceManager]; + + FQuerySpec *limit2FooQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo") params:[[FQueryParams defaultInstance] limitToFirst:2]]; + FQuerySpec *limit3FooQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo") params:[[FQueryParams defaultInstance] limitToFirst:3]]; + + [manager setQueryActive:limit2FooQuery]; + [manager updateServerCacheWithNode:NODE((@{@"a": @1, @"b": @2, @"c": @3, @"d": @4})) forQuery:limit2FooQuery]; + [manager setTrackedQueryKeys:[NSSet setWithArray:@[@"a", @"b"]] forQuery:limit2FooQuery]; + + FCacheNode *cache = [manager serverCacheForQuery:limit3FooQuery]; + XCTAssertFalse(cache.isFullyInitialized); + XCTAssertEqualObjects(cache.node, NODE((@{@"a": @1, @"b": @2}))); +} + +- (void)testNoLimitNonDefaultQueryIsTreatedAsDefaultQuery { + FPersistenceManager *manager = [self newTestPersistenceManager]; + + FQuerySpec *defaultQuery = [FQuerySpec defaultQueryAtPath:PATH(@"foo")]; + id<FIndex> index = [[FPathIndex alloc] initWithPath:PATH(@"index-key")]; + FQuerySpec *orderByQuery = [[FQuerySpec alloc] initWithPath:PATH(@"foo") + params:[[FQueryParams defaultInstance] orderBy:index]]; + [manager setQueryActive:defaultQuery]; + [manager updateServerCacheWithNode:NODE((@{@"foo": @1, @"bar": @2})) + forQuery:defaultQuery]; + [manager setQueryComplete:defaultQuery]; + + FCacheNode *node = [manager serverCacheForQuery:orderByQuery]; + + XCTAssertEqualObjects(node.node, NODE((@{@"foo": @1, @"bar": @2}))); + XCTAssertTrue(node.isFullyInitialized); + XCTAssertFalse(node.isFiltered); + XCTAssertTrue([node.indexedNode hasIndex:orderByQuery.index]); +} + +- (void)testApplyUserMergeUsesRelativePath { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + + id<FNode> initialData = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"baz-value"}})); + [engine updateServerCache:initialData atPath:PATH(@"") merge:NO]; + + FPersistenceManager *manager = [[FPersistenceManager alloc] initWithStorageEngine:engine + cachePolicy:[FNoCachePolicy noCachePolicy]]; + + FCompoundWrite *update = [FCompoundWrite compoundWriteWithValueDictionary:@{@"baz": @"new-baz", @"qux": @"qux"}]; + [manager applyUserMerge:update toServerCacheAtPath:PATH(@"foo")]; + + id<FNode> expected = NODE((@{@"foo": @{ @"bar": @"bar-value", @"baz": @"new-baz", @"qux": @"qux"}})); + id<FNode> actual = [engine serverCacheAtPath:PATH(@"")]; + XCTAssertEqualObjects(actual, expected); +} + +@end diff --git a/Example/Database/Tests/Unit/FPruneForestTest.m b/Example/Database/Tests/Unit/FPruneForestTest.m new file mode 100644 index 0000000..0694ba7 --- /dev/null +++ b/Example/Database/Tests/Unit/FPruneForestTest.m @@ -0,0 +1,98 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import <XCTest/XCTest.h> + +#import "FPruneForest.h" +#import "FPath.h" + +@interface FPruneForestTest : XCTestCase + +@end + +@implementation FPruneForestTest + +- (void) testEmptyDoesNotAffectAnyPaths { + FPruneForest *forest = [FPruneForest empty]; + XCTAssertFalse([forest affectsPath:[FPath empty]]); + XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"foo"]]); +} + +- (void) testPruneAffectsPath { + FPruneForest *forest = [FPruneForest empty]; + forest = [forest prunePath:[FPath pathWithString:@"foo/bar"]]; + forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]]; + XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo"]]); + XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"baz"]]); + XCTAssertFalse([forest affectsPath:[FPath pathWithString:@"baz/bar"]]); + XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar"]]); + XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar/baz"]]); + XCTAssertTrue([forest affectsPath:[FPath pathWithString:@"foo/bar/qux"]]); +} + +- (void) testPruneAnythingWorks { + FPruneForest *empty = [FPruneForest empty]; + XCTAssertFalse([empty prunesAnything]); + XCTAssertTrue([[empty prunePath:[FPath pathWithString:@"foo"]] prunesAnything]); + XCTAssertFalse([[[empty prunePath:[FPath pathWithString:@"foo/bar"]] keepPath:[FPath pathWithString:@"foo"]] prunesAnything]); + XCTAssertTrue([[[empty prunePath:[FPath pathWithString:@"foo"]] keepPath:[FPath pathWithString:@"foo/bar"]] prunesAnything]); +} + +- (void) testKeepUnderPruneWorks { + FPruneForest *forest = [FPruneForest empty]; + forest = [forest prunePath:[FPath pathWithString:@"foo/bar"]]; + forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]]; + forest = [forest keepAll:[NSSet setWithArray:@[@"qux", @"quu"]] atPath:[FPath pathWithString:@"foo/bar"]]; +} + +- (void) testPruneUnderKeepThrows { + FPruneForest *forest = [FPruneForest empty]; + forest = [forest prunePath:[FPath pathWithString:@"foo"]]; + forest = [forest keepPath:[FPath pathWithString:@"foo/bar"]]; + XCTAssertThrows([forest prunePath:[FPath pathWithString:@"foo/bar/baz"]]); + NSSet *children = [NSSet setWithArray:@[@"qux", @"quu"]]; + XCTAssertThrows([forest pruneAll:children atPath:[FPath pathWithString:@"foo/bar"]]); +} + +- (void) testChildKeepsPruneInfo { + FPruneForest *forest = [FPruneForest empty]; + forest = [forest keepPath:[FPath pathWithString:@"foo/bar"]]; + XCTAssertTrue([[forest child:@"foo"] affectsPath:[FPath pathWithString:@"bar"]]); + XCTAssertTrue([[[forest child:@"foo"] child:@"bar"] affectsPath:[FPath pathWithString:@""]]); + XCTAssertTrue([[[[forest child:@"foo"] child:@"bar"] child:@"baz"] affectsPath:[FPath pathWithString:@""]]); + + forest = [[FPruneForest empty] prunePath:[FPath pathWithString:@"foo/bar"]]; + XCTAssertTrue([[forest child:@"foo"] affectsPath:[FPath pathWithString:@"bar"]]); + XCTAssertTrue([[[forest child:@"foo"] child:@"bar"] affectsPath:[FPath pathWithString:@""]]); + XCTAssertTrue([[[[forest child:@"foo"] child:@"bar"] child:@"baz"] affectsPath:[FPath pathWithString:@""]]); + + XCTAssertFalse([[forest child:@"non-existent"] affectsPath:[FPath pathWithString:@""]]); +} + +- (void) testShouldPruneWorks { + FPruneForest *forest = [FPruneForest empty]; + forest = [forest prunePath:[FPath pathWithString:@"foo"]]; + forest = [forest keepPath:[FPath pathWithString:@"foo/bar/baz"]]; + XCTAssertTrue([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo"]]); + XCTAssertTrue([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo/bar"]]); + XCTAssertFalse([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"foo/bar/baz"]]); + XCTAssertFalse([forest shouldPruneUnkeptDescendantsAtPath:[FPath pathWithString:@"qux"]]); +} + + +@end diff --git a/Example/Database/Tests/Unit/FPruningTest.m b/Example/Database/Tests/Unit/FPruningTest.m new file mode 100644 index 0000000..d1e7354 --- /dev/null +++ b/Example/Database/Tests/Unit/FPruningTest.m @@ -0,0 +1,293 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +#import "FLevelDBStorageEngine.h" +#import "FTestHelpers.h" +#import "FPruneForest.h" +#import "FEmptyNode.h" +#import "FMockStorageEngine.h" + +@interface FPruningTest : XCTestCase + +@end + +static id<FNode> ABC_NODE = nil; +static id<FNode> DEF_NODE = nil; +static id<FNode> A_NODE = nil; +static id<FNode> D_NODE = nil; +static id<FNode> BC_NODE = nil; +static id<FNode> LARGE_NODE = nil; + +@implementation FPruningTest + ++ (void)initStatics { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ABC_NODE = NODE((@{@"a": @{@"aa": @1.1, @"ab": @1.2}, @"b": @2, @"c": @3})); + DEF_NODE = NODE((@{@"d": @4, @"e": @5, @"f": @6})); + A_NODE = NODE((@{@"a": @{@"aa": @1.1, @"ab": @1.2}})); + D_NODE = NODE(@{@"d": @4}); + LARGE_NODE = [FTestHelpers leafNodeOfSize:5*1024*1024]; + BC_NODE = [ABC_NODE updateImmediateChild:@"a" withNewChild:[FEmptyNode emptyNode]]; + }); +} + +- (void)runWithDb:(void (^)(id<FStorageEngine>engine))block { + [FPruningTest initStatics]; + { + // Run with level DB implementation + FLevelDBStorageEngine *engine = [[FLevelDBStorageEngine alloc] initWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"purge-tests"]]; + block(engine); + [engine purgeEverything]; + [engine close]; + } + { + // Run with mock implementation + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + block(engine); + [engine close]; + + } +} + +- (FPruneForest *)prune:(NSString *)pathStr { + return [[FPruneForest empty] prunePath:PATH(pathStr)]; +} + +- (FPruneForest *)prune:(NSString *)path exceptRelative:(NSArray *)except { + __block FPruneForest *pruneForest = [FPruneForest empty]; + pruneForest = [pruneForest prunePath:PATH(path)]; + [except enumerateObjectsUsingBlock:^(NSString *keepPath, NSUInteger idx, BOOL *stop) { + pruneForest = [pruneForest keepPath:[PATH(path) childFromString:keepPath]]; + }]; + return pruneForest; +} + +// Write document at root, prune it. +- (void)test010 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO]; + [engine pruneCache:[self prune:@""] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]); + }]; +} + +// Write document at /x, prune it via PruneForest for /x, at root. +- (void)test020 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine pruneCache:[self prune:@"x"] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]); + }]; +} + +// Write document at /x, prune it via PruneForest for root, at /x. +- (void)test030 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine pruneCache:[self prune:@""] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]); + }]; +} + +// Write document at /x, prune it via PruneForest for root, at root +- (void)test040 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine pruneCache:[self prune:@""] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]); + }]; +} + +// Write document at /x/y, prune it via PruneForest for /y, at /x +- (void)test050 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO]; + [engine pruneCache:[self prune:@"y"] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], [FEmptyNode emptyNode]); + }]; +} + +// Write abc at /x/y, prune /x/y except b,c via PruneForest for /x/y -b,c, at root +- (void)test060 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO]; + [engine pruneCache:[self prune:@"x/y" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], BC_NODE); + }]; +} + +// Write abc at /x/y, prune /x/y except b,c via PruneForest for /y -b,c, at /x +- (void)test070 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO]; + [engine pruneCache:[self prune:@"y" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], BC_NODE); + }]; +} + +// Write abc at /x/y, prune /x/y except not-there via PruneForest for /x/y -d, at root +- (void)test080 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x/y") merge:NO]; + [engine pruneCache:[self prune:@"x/y" exceptRelative:@[@"not-there"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x/y")], [FEmptyNode emptyNode]); + }]; +} + +// Write abc at / and def at /a, prune all via PruneForest for / at root +- (void)test090 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"a") merge:NO]; + [engine pruneCache:[self prune:@""] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], [FEmptyNode emptyNode]); + }]; +} + +// Write abc at / and def at /a, prune all except b,c via PruneForest for root -b,c, at root +- (void)test100 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"a") merge:NO]; + [engine pruneCache:[self prune:@"" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"")], BC_NODE); + }]; +} + +// Write abc at /x and def at /x/a, prune /x except b,c via PruneForest for /x -b,c, at root +- (void)test110 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO]; + [engine pruneCache:[self prune:@"x" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], BC_NODE); + }]; +} + +// Write abc at /x and def at /x/a, prune /x except b,c via PruneForest for root -b,c, at /x +- (void)test120 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO]; + [engine pruneCache:[self prune:@"" exceptRelative:@[@"b", @"c"]] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], BC_NODE); + }]; +} + +// Write abc at /x and def at /x/a, prune /x except a via PruneForest for /x -a, at root +- (void)test130 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [ABC_NODE updateImmediateChild:@"a" withNewChild:DEF_NODE]); + [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:DEF_NODE]); + }]; +} + +// Write abc at /x and def at /x/a, prune /x except a via PruneForest for root -a, at /x +- (void)test140 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO]; + [engine pruneCache:[self prune:@"" exceptRelative:@[@"a"]] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:DEF_NODE]); + }]; +} + +// Write abc at /x and def at /x/a, prune /x except a/d via PruneForest for /x -a/d, at root +- (void)test150 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO]; + [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a/d"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:D_NODE]); + }]; +} + +// Write abc at /x and def at /x/a, prune /x except a/d via PruneForest for / -a/d, at /x +- (void)test160 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a") merge:NO]; + [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/d"]] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:D_NODE]); + }]; +} + +// Write abc at /x and def at /x/a/aa, prune /x except a via PruneForest for /x -a, at root +- (void)test170 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO]; + [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [A_NODE updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]); + }]; +} + +// Write abc at /x and def at /x/a/aa, prune /x except a via PruneForest for / -a, at /x +- (void)test180 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO]; + [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/aa"]] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]); + }]; +} + +// Write abc at /x and def at /x/a/aa, prune /x except a/aa via PruneForest for /x -a/aa, at root +- (void)test190 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO]; + [engine pruneCache:[self prune:@"x" exceptRelative:@[@"a/aa"]] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]); + }]; +} + +// Write abc at /x and def at /x/a/aa, prune /x except a/aa via PruneForest for / -a/aa, at /x +- (void)test200 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:DEF_NODE atPath:PATH(@"x/a/aa") merge:NO]; + [engine pruneCache:[self prune:@"" exceptRelative:@[@"a/aa"]] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateChild:PATH(@"a/aa") withNewChild:DEF_NODE]); + }]; +} + +// Write large node at /x, prune x via PruneForest for x at root +- (void)test210 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:LARGE_NODE atPath:PATH(@"x") merge:NO]; + [engine pruneCache:[self prune:@"x"] atPath:PATH(@"")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [FEmptyNode emptyNode]); + }]; +} + +// Write abc at x and large node at /x/a, prune x except a via PruneForest for / -a, at x +- (void)test220 { + [self runWithDb:^(id<FStorageEngine> engine) { + [engine updateServerCache:ABC_NODE atPath:PATH(@"x") merge:NO]; + [engine updateServerCache:LARGE_NODE atPath:PATH(@"x/a") merge:NO]; + [engine pruneCache:[self prune:@"" exceptRelative:@[@"a"]] atPath:PATH(@"x")]; + XCTAssertEqualObjects([engine serverCacheAtPath:PATH(@"x")], [[FEmptyNode emptyNode] updateImmediateChild:@"a" withNewChild:LARGE_NODE]); + }]; +} + +@end diff --git a/Example/Database/Tests/Unit/FQueryParamsTest.m b/Example/Database/Tests/Unit/FQueryParamsTest.m new file mode 100644 index 0000000..8c98ff9 --- /dev/null +++ b/Example/Database/Tests/Unit/FQueryParamsTest.m @@ -0,0 +1,162 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> + +#import "FQueryParams.h" +#import "FIndex.h" +#import "FPriorityIndex.h" +#import "FValueIndex.h" +#import "FLeafNode.h" +#import "FPathIndex.h" +#import "FSnapshotUtilities.h" +#import "FKeyIndex.h" +#import "FEmptyNode.h" + +@interface FQueryParamsTest : XCTestCase + +@end + +@implementation FQueryParamsTest + +- (void)testQueryParamsEquals { + { // Limit equals + FQueryParams *params1 = [[FQueryParams defaultInstance] limitToLast:10]; + FQueryParams *params2 = [[FQueryParams defaultInstance] limitTo:10]; + FQueryParams *params3 = [[FQueryParams defaultInstance] limitToFirst:10]; + FQueryParams *params4 = [[FQueryParams defaultInstance] limitToLast:11]; + XCTAssertEqualObjects(params1, params2); + XCTAssertEqual(params1.hash, params2.hash); + XCTAssertFalse([params1 isEqual:params3]); + XCTAssertFalse([params1 isEqual:params4]); + } + + { // Index equals + FQueryParams *params1 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]]; + FQueryParams *params2 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]]; + FQueryParams *params3 = [[FQueryParams defaultInstance] orderBy:[FKeyIndex keyIndex]]; + XCTAssertEqualObjects(params1, params2); + XCTAssertEqual(params1.hash, params2.hash); + XCTAssertFalse([params1 isEqual:params3]); + } + + { // startAt equals + FQueryParams *params1 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value"]]; + FQueryParams *params2 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value"] childKey:nil]; + FQueryParams *params3 = [[FQueryParams defaultInstance] startAt:[FSnapshotUtilities nodeFrom:@"value-2"]]; + XCTAssertEqualObjects(params1, params2); + XCTAssertEqual(params1.hash, params2.hash); + XCTAssertFalse([params1 isEqual:params3]); + } + + { // startAt with childkey equals + FQueryParams *params1 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"key"]; + FQueryParams *params2 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"key"]; + FQueryParams *params3 = [[FQueryParams defaultInstance] startAt:[FEmptyNode emptyNode] childKey:@"other-key"]; + XCTAssertEqualObjects(params1, params2); + XCTAssertEqual(params1.hash, params2.hash); + XCTAssertFalse([params1 isEqual:params3]); + } + + { // endAt equals + FQueryParams *params1 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value"]]; + FQueryParams *params2 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value"] childKey:nil]; + FQueryParams *params3 = [[FQueryParams defaultInstance] endAt:[FSnapshotUtilities nodeFrom:@"value-2"]]; + XCTAssertEqualObjects(params1, params2); + XCTAssertEqual(params1.hash, params2.hash); + XCTAssertFalse([params1 isEqual:params3]); + } + + { // endAt with childkey equals + FQueryParams *params1 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"key"]; + FQueryParams *params2 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"key"]; + FQueryParams *params3 = [[FQueryParams defaultInstance] endAt:[FEmptyNode emptyNode] childKey:@"other-key"]; + XCTAssertEqualObjects(params1, params2); + XCTAssertEqual(params1.hash, params2.hash); + XCTAssertFalse([params1 isEqual:params3]); + } + + { // Limit/startAt equals + FQueryParams *params1 = [[[FQueryParams defaultInstance] limitToFirst:10] startAt:[FSnapshotUtilities nodeFrom:@"value"]]; + FQueryParams *params2 = [[[FQueryParams defaultInstance] limitTo:10] startAt:[FSnapshotUtilities nodeFrom:@"value"]]; + FQueryParams *params3 = [[[FQueryParams defaultInstance] limitTo:10] startAt:[FSnapshotUtilities nodeFrom:@"value-2"]]; + XCTAssertEqualObjects(params1, params2); + XCTAssertEqual(params1.hash, params2.hash); + XCTAssertFalse([params1 isEqual:params3]); + } +} + +- (void)testFromDictionaryEquals { + FQueryParams *params1 = [[[[[FQueryParams defaultInstance] limitToLast:10] + startAt:[FSnapshotUtilities nodeFrom:@"start-value"] childKey:@"child-key-2"] + endAt:[FSnapshotUtilities nodeFrom:@"end-value"] childKey:@"child-key-2"] + orderBy:[FKeyIndex keyIndex]]; + XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]); + XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash); +} + +- (void)testCanCreateAllIndexes { + FQueryParams *params1 = [[FQueryParams defaultInstance] orderBy:[FKeyIndex keyIndex]]; + FQueryParams *params2 = [[FQueryParams defaultInstance] orderBy:[FValueIndex valueIndex]]; + FQueryParams *params3 = [[FQueryParams defaultInstance] orderBy:[FPriorityIndex priorityIndex]]; + FQueryParams *params4 = [[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:[[FPath alloc] initWith:@"subkey"]]]; + XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]); + XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params2.wireProtocolParams]); + XCTAssertEqualObjects(params3, [FQueryParams fromQueryObject:params3.wireProtocolParams]); + XCTAssertEqualObjects(params4, [FQueryParams fromQueryObject:params4.wireProtocolParams]); + XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash); + XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params2.wireProtocolParams].hash); + XCTAssertEqual(params3.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash); + XCTAssertEqual(params4.hash, [FQueryParams fromQueryObject:params4.wireProtocolParams].hash); +} + +- (void)testDifferentLimits { + FQueryParams *params1 = [[FQueryParams defaultInstance] limitToFirst:10]; + FQueryParams *params2 = [[FQueryParams defaultInstance] limitToLast:10]; + FQueryParams *params3 = [[FQueryParams defaultInstance] limitTo:10]; + XCTAssertEqualObjects(params1, [FQueryParams fromQueryObject:params1.wireProtocolParams]); + XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params2.wireProtocolParams]); + XCTAssertEqualObjects(params3, [FQueryParams fromQueryObject:params3.wireProtocolParams]); + // 2 and 3 are equivalent + XCTAssertEqualObjects(params2, [FQueryParams fromQueryObject:params3.wireProtocolParams]); + + XCTAssertEqual(params1.hash, [FQueryParams fromQueryObject:params1.wireProtocolParams].hash); + XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params2.wireProtocolParams].hash); + XCTAssertEqual(params3.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash); + // 2 and 3 are equivalent + XCTAssertEqual(params2.hash, [FQueryParams fromQueryObject:params3.wireProtocolParams].hash); +} + +- (void)testStartAtNullIsSerializable { + FQueryParams *params = [FQueryParams defaultInstance]; + params = [params startAt:[FEmptyNode emptyNode] childKey:@"key"]; + NSDictionary *dict = [params wireProtocolParams]; + FQueryParams *parsed = [FQueryParams fromQueryObject:dict]; + XCTAssertEqualObjects(parsed, params); + XCTAssertTrue([parsed hasStart]); +} + +- (void)testEndAtNullIsSerializable { + FQueryParams *params = [FQueryParams defaultInstance]; + params = [params endAt:[FEmptyNode emptyNode] childKey:@"key"]; + NSDictionary *dict = [params wireProtocolParams]; + FQueryParams *parsed = [FQueryParams fromQueryObject:dict]; + XCTAssertEqualObjects(parsed, params); + XCTAssertTrue([parsed hasEnd]); +} + +@end diff --git a/Example/Database/Tests/Unit/FRangeMergeTest.m b/Example/Database/Tests/Unit/FRangeMergeTest.m new file mode 100644 index 0000000..32ea6ad --- /dev/null +++ b/Example/Database/Tests/Unit/FRangeMergeTest.m @@ -0,0 +1,271 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> + +#import <XCTest/XCTest.h> + +#import "FRangeMerge.h" +#import "FNode.h" +#import "FTestHelpers.h" +#import "FEmptyNode.h" + +@interface FRangeMergeTest : XCTestCase + +@end + +@implementation FRangeMergeTest + +- (void)testSmokeTest { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @{@"a": @{@"deep-a-1": @1, @"deep-a-2": @2}, @"b": @"b", @"c": @"c", @"d": @"d"}, @"quu": @"quu-value"})); + + id<FNode> updates = NODE((@{@"foo": @{@"a": @{@"deep-a-2": @"new-a-2", @"deep-a-3": @3}, @"b-2": @"new-b", @"c": @"new-c" }})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo/a/deep-a-1") end:PATH(@"foo/c") updates:updates]; + + id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @{@"a": @{@"deep-a-1": @1, @"deep-a-2": @"new-a-2", @"deep-a-3": @3}, @"b-2": @"new-b", @"c": @"new-c", @"d": @"d"}, @"quu": @"quu-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testStartIsExclusive { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"})); + + id<FNode> updates = NODE((@{@"foo": @"new-foo-value"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; + + id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @"new-foo-value", @"quu": @"quu-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testStartIsExclusiveButIncludesChildren { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"})); + + id<FNode> updates = NODE((@{@"bar": @{@"bar-child": @"bar-child-value"}, @"foo": @"new-foo-value"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; + + id<FNode> expected = NODE((@{@"bar": @{@"bar-child": @"bar-child-value"}, @"foo": @"new-foo-value", @"quu": @"quu-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testEndIsInclusive { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"})); + + id<FNode> updates = NODE((@{@"baz": @"baz-value"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; // foo should be deleted + + id<FNode> expected = NODE((@{@"bar": @"bar-value", @"baz": @"baz-value", @"quu": @"quu-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testEndIsInclusiveButExcludesChildren { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @{@"foo-child": @"foo-child-value"}, @"quu": @"quu-value"})); + + id<FNode> updates = NODE((@{@"baz": @"baz-value"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; // foo should be deleted + + id<FNode> expected = NODE((@{@"bar": @"bar-value", @"baz": @"baz-value", @"foo": @{@"foo-child": @"foo-child-value"}, @"quu": @"quu-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testCanUpdateLeafNode { + id<FNode> node = NODE(@"leaf-value"); + + id<FNode> updates = NODE((@{@"bar": @"bar-value"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo") updates:updates]; + id<FNode> expected = NODE((@{@"bar": @"bar-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testCanReplaceLeafNodeWithLeafNode{ + id<FNode> node = NODE(@"leaf-value"); + + id<FNode> updates = NODE(@"new-leaf-value"); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"") updates:updates]; + id<FNode> expected = NODE(@"new-leaf-value"); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testLeafsAreUpdatedWhenRangesIncludeDeeperPath { + id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}})); + + id<FNode> updates = NODE((@{@"foo": @{@"bar": @"new-bar-value"}})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/bar/deep") updates:updates]; + + id<FNode> expected = NODE((@{@"foo": @{@"bar": @"new-bar-value"}})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testLeafsAreNotUpdatedWhenRangesIncludeDeeperPaths { + id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}})); + + id<FNode> updates = NODE((@{@"foo": @{@"bar": @"new-bar-value"}})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo/bar") end:PATH(@"foo/bar/deep") updates:updates]; + + id<FNode> expected = NODE((@{@"foo": @{@"bar": @"bar-value"}})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testUpdatingEntireRangeUpdatesEverything { + id<FNode> node = [FEmptyNode emptyNode]; + + id<FNode> updates = NODE((@{@"foo": @"foo-value", @"bar": @{@"child": @"bar-child-value"}})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:nil updates:updates]; + + id<FNode> expected = NODE((@{@"foo": @"foo-value", @"bar": @{@"child": @"bar-child-value"}})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testUpdatingRangeWithUnboundedLeftPostWorks { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"})); + + id<FNode> updates = NODE((@{@"bar": @"new-bar"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates]; + + id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testUpdatingRangeWithRightPostChildOfLeftPostWorks { + id<FNode> node = NODE((@{@"foo": @{@"a": @"a", @"b": @{@"1": @"1", @"2": @"2"}, @"c": @"c"}})); + + id<FNode> updates = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1"}}})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/b/1") updates:updates]; + + id<FNode> expected = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1", @"2": @"2"}, @"c": @"c"}})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testUpdatingRangeWithRightPostChildOfLeftPostWorksWithIntegerKeys { + id<FNode> node = NODE((@{@"foo": @{@"a": @"a", @"b": @{@"1": @"1", @"2": @"2", @"10": @"10"}, @"c": @"c"}})); + + id<FNode> updates = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1"}}})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"foo") end:PATH(@"foo/b/2") updates:updates]; + + id<FNode> expected = NODE((@{@"foo": @{@"a": @"new-a", @"b": @{@"1": @"new-1", @"10": @"10"}, @"c": @"c"}})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testUpdatingLeafIncludesPriority { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @"quu": @"quu-value"})); + + id<FNode> updates = NODE((@{@"foo": @{@".value": @"new-foo", @".priority": @"prio"}})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"bar") end:PATH(@"foo") updates:updates]; + + id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @{@".value": @"new-foo", @".priority": @"prio" }, @"quu": @"quu-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testUpdatingPriorityInChildrenNodeWorks { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"})); + + id<FNode> updates = NODE((@{@"bar": @"new-bar", @".priority": @"prio"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates]; + + id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value", @".priority": @"prio"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +// TODO: this test should actuall;y work, but priorities on empty nodes are ignored :( +- (void)updatingPriorityInChildrenNodeWorksAlone { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value"})); + + id<FNode> updates = NODE((@{@".priority": @"prio" })); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@".priority") updates:updates]; + + id<FNode> expected = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @".priority": @"prio"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testUpdatingPriorityOnInitiallyEmptyNodeDoesNotBreak { + id<FNode> node = NODE((@{})); + + id<FNode> updates = NODE((@{@".priority": @"prio", @"foo": @"foo-value" })); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo") updates:updates]; + + id<FNode> expected = NODE((@{@"foo": @"foo-value", @".priority": @"prio"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testPriorityIsDeletedWhenIncludedInChildrenRange { + id<FNode> node = NODE((@{@"bar": @"bar-value", @"foo": @"foo-value", @".priority": @"prio"})); + + id<FNode> updates = NODE((@{@"bar": @"new-bar"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"bar") updates:updates]; // deletes priority + + id<FNode> expected = NODE((@{@"bar": @"new-bar", @"foo": @"foo-value"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testPriorityIsIncludedInOpenStart { + id<FNode> node = NODE((@{@"foo": @{@"bar": @"bar-value"}})); + + id<FNode> updates = NODE((@{@".priority": @"prio", @"baz": @"baz"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:nil end:PATH(@"foo/bar") updates:updates]; + + id<FNode> expected = NODE((@{@"baz": @"baz", @".priority": @"prio"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +- (void)testPriorityIsIncludedInOpenEnd { + id<FNode> node = NODE(@"leaf-node"); + + id<FNode> updates = NODE((@{@".priority": @"prio", @"foo": @"bar"})); + + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:PATH(@"/") end:nil updates:updates]; + + id<FNode> expected = NODE((@{@"foo": @"bar", @".priority": @"prio"})); + id<FNode> actual = [merge applyToNode:node]; + XCTAssertEqualObjects(actual, expected); +} + +@end diff --git a/Example/Database/Tests/Unit/FRepoInfoTest.m b/Example/Database/Tests/Unit/FRepoInfoTest.m new file mode 100644 index 0000000..94e6a70 --- /dev/null +++ b/Example/Database/Tests/Unit/FRepoInfoTest.m @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import <XCTest/XCTest.h> + +#import "FRepoInfo.h" +#import "FTestHelpers.h" +@interface FRepoInfoTest : XCTestCase + +@end + +@implementation FRepoInfoTest + +- (void) testGetConnectionUrl { + FRepoInfo *info = [[FRepoInfo alloc] initWithHost:@"test-namespace.example.com" + isSecure:NO + withNamespace:@"tests"]; + XCTAssertEqualObjects(info.connectionURL, @"ws://test-namespace.example.com/.ws?v=5&ns=tests", + @"getConnection works"); +} + +- (void) testGetConnectionUrlWithLastSession { + FRepoInfo *info = [[FRepoInfo alloc] initWithHost:@"tests-namespace.example.com" + isSecure:NO + withNamespace:@"tests"]; + XCTAssertEqualObjects([info connectionURLWithLastSessionID:@"testsession"], + @"ws://tests-namespace.example.com/.ws?v=5&ns=tests&ls=testsession", + @"getConnectionWithLastSession works"); +} +@end diff --git a/Example/Database/Tests/Unit/FSparseSnapshotTests.h b/Example/Database/Tests/Unit/FSparseSnapshotTests.h new file mode 100644 index 0000000..1f0acb2 --- /dev/null +++ b/Example/Database/Tests/Unit/FSparseSnapshotTests.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestBase.h" + +@interface FSparseSnapshotTests : FTestBase + +@end diff --git a/Example/Database/Tests/Unit/FSparseSnapshotTests.m b/Example/Database/Tests/Unit/FSparseSnapshotTests.m new file mode 100644 index 0000000..ab22c0d --- /dev/null +++ b/Example/Database/Tests/Unit/FSparseSnapshotTests.m @@ -0,0 +1,207 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FSparseSnapshotTests.h" +#import "FSparseSnapshotTree.h" +#import "FSnapshotUtilities.h" +#import "FEmptyNode.h" + +@implementation FSparseSnapshotTests + +- (void) testBasicRememberAndFind { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + FPath* path = [[FPath alloc] initWith:@"a/b"]; + id<FNode> node = [FSnapshotUtilities nodeFrom:@"sdfsd"]; + + [st rememberData:node onPath:path]; + id<FNode> found = [st findPath:path]; + XCTAssertFalse([found isEmpty], @"Should find node"); + found = [st findPath:path.parent]; + XCTAssertTrue(found == nil, @"Should not find a node"); +} + +- (void) testFindInsideAnExistingSnapshot { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + FPath* path = [[FPath alloc] initWith:@"t/tt"]; + id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"a": @"sdfsd", @"x": @5, @"999i": @YES}]; + id<FNode> update = [FSnapshotUtilities nodeFrom:@{@"goats": @88}]; + node = [node updateImmediateChild:@"apples" withNewChild:update]; + [st rememberData:node onPath:path]; + + id<FNode> found = [st findPath:path]; + XCTAssertFalse([found isEmpty], @"Should find the node we set"); + found = [st findPath:[path childFromString:@"a"]]; + XCTAssertTrue([[found val] isEqualToString:@"sdfsd"], @"Find works inside data snapshot"); + found = [st findPath:[path childFromString:@"999i"]]; + XCTAssertTrue([[found val] isEqualToNumber:@YES], @"Find works inside data snapshot"); + found = [st findPath:[path childFromString:@"apples"]]; + XCTAssertFalse([found isEmpty], @"Should find the node we set"); + found = [st findPath:[path childFromString:@"apples/goats"]]; + XCTAssertTrue([[found val] isEqualToNumber:@88], @"Find works inside data snapshot"); +} + +- (void) testWriteASnapshotInsideASnapshot { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]]; + [st rememberData:[FSnapshotUtilities nodeFrom:@19] onPath:[[FPath alloc] initWith:@"t/a/rr"]]; + id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t/a/b"]]; + XCTAssertTrue([[found val] isEqualToString:@"v"], @"Find inside snap"); + found = [st findPath:[[FPath alloc] initWith:@"t/a/rr"]]; + XCTAssertTrue([[found val] isEqualToNumber:@19], @"Find inside snap"); +} + +- (void) testWriteANullValueAndConfirmItIsRemembered { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:[FSnapshotUtilities nodeFrom:[NSNull null]] onPath:[[FPath alloc] initWith:@"awq/fff"]]; + id<FNode> found = [st findPath:[[FPath alloc] initWith:@"awq/fff"]]; + XCTAssertTrue([found isEmpty], @"Empty node"); + found = [st findPath:[[FPath alloc] initWith:@"awq/sdf"]]; + XCTAssertTrue(found == nil, @"No node here"); + found = [st findPath:[[FPath alloc] initWith:@"awq/fff/jjj"]]; + XCTAssertTrue([found isEmpty], @"Empty node"); + found = [st findPath:[[FPath alloc] initWith:@"awq/sdf/sdj/q"]]; + XCTAssertTrue(found == nil, @"No node here"); +} + +- (void) testOverwriteWithNullAndConfirmItIsRemembered { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]]; + id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]]; + XCTAssertFalse([found isEmpty], @"non-empty node"); + [st rememberData:[FSnapshotUtilities nodeFrom:[NSNull null]] onPath:[[FPath alloc] initWith:@"t"]]; + found = [st findPath:[[FPath alloc] initWith:@"t"]]; + XCTAssertTrue([found isEmpty], @"Empty node"); +} + +- (void) testSimpleRememberAndForget { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]]; + id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]]; + XCTAssertFalse([found isEmpty], @"non-empty node"); + [st forgetPath:[[FPath alloc] initWith:@"t"]]; + found = [st findPath:[[FPath alloc] initWith:@"t"]]; + XCTAssertTrue(found == nil, @"node is gone"); +} + +- (void) testForgetTheRoot { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v"}}] onPath:[[FPath alloc] initWith:@"t"]]; + id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t"]]; + XCTAssertFalse([found isEmpty], @"non-empty node"); + found = [st findPath:[[FPath alloc] initWith:@""]]; + XCTAssertTrue(found == nil, @"node is gone"); +} + +- (void) testForgetSnapshotInsideSnapshot { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:[FSnapshotUtilities nodeFrom:@{@"a": @{@"b": @"v", @"c": @9, @"art": @NO}}] onPath:[[FPath alloc] initWith:@"t"]]; + id<FNode> found = [st findPath:[[FPath alloc] initWith:@"t/a/c"]]; + XCTAssertFalse([found isEmpty], @"non-empty node"); + found = [st findPath:[[FPath alloc] initWith:@"t"]]; + XCTAssertFalse([found isEmpty], @"non-empty node"); + [st forgetPath:PATH(@"t/a/c")]; + XCTAssertTrue([st findPath:PATH(@"t")] == nil, @"no more node here"); + XCTAssertTrue([st findPath:PATH(@"t/a")] == nil, @"no more node here"); + XCTAssertTrue([[[st findPath:PATH(@"t/a/b")] val] isEqualToString:@"v"], @"child still exists"); + XCTAssertTrue([st findPath:PATH(@"t/a/c")] == nil, @"no more node here"); + XCTAssertTrue([[[st findPath:PATH(@"t/a/art")] val] isEqualToNumber:@NO], @"child still exists"); +} + +- (void) testPathShallowerThanSnapshots { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:NODE(@NO) onPath:PATH(@"t/x1")]; + [st rememberData:NODE(@YES) onPath:PATH(@"t/x2")]; + + [st forgetPath:PATH(@"t")]; + XCTAssertTrue([st findPath:PATH(@"t")] == nil, @"No more node here"); +} + +- (void) testIterateChildren { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + id<FNode> node = [FSnapshotUtilities nodeFrom:@{@"b": @"v", @"c": @9, @"art": @NO}]; + [st rememberData:node onPath:PATH(@"t")]; + [st rememberData:[FEmptyNode emptyNode] onPath:PATH(@"q")]; + + __block int num = 0; + __block BOOL gotT = NO; + __block BOOL gotQ = NO; + [st forEachChild:^(NSString* key, FSparseSnapshotTree* tree) { + num++; + if ([key isEqualToString:@"t"]) { + gotT = YES; + } else if ([key isEqualToString:@"q"]) { + gotQ = YES; + } else { + XCTFail(@"Unknown child"); + } + }]; + + XCTAssertTrue(gotT, @"Saw t"); + XCTAssertTrue(gotQ, @"Saw q"); + XCTAssertTrue(num == 2, @"Saw two children"); +} + +- (void) testIterateTrees { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + __block int count = 0; + [st forEachTreeAtPath:PATH(@"") do:^(FPath *path, id<FNode> data) { + count++; + }]; + XCTAssertTrue(count == 0, @"No trees to iterate through"); + + [st rememberData:NODE(@1) onPath:PATH(@"t")]; + [st rememberData:NODE(@2) onPath:PATH(@"a/b")]; + [st rememberData:NODE(@3) onPath:PATH(@"a/x/g")]; + [st rememberData:NODE([NSNull null]) onPath:PATH(@"a/x/null")]; + + __block int num = 0; + __block BOOL got1 = NO; + __block BOOL got2 = NO; + __block BOOL got3 = NO; + __block BOOL gotNull = NO; + + [st forEachTreeAtPath:PATH(@"q") do:^(FPath *path, id<FNode> data) { + num++; + NSString* pathString = [path description]; + if ([pathString isEqualToString:@"/q/t"]) { + got1 = YES; + XCTAssertTrue([[data val] isEqualToNumber:@1], @"got 1"); + } else if ([pathString isEqualToString:@"/q/a/b"]) { + got2 = YES; + XCTAssertTrue([[data val] isEqualToNumber:@2], @"got 2"); + } else if ([pathString isEqualToString:@"/q/a/x/g"]) { + got3 = YES; + XCTAssertTrue([[data val] isEqualToNumber:@3], @"got 3"); + } else if ([pathString isEqualToString:@"/q/a/x/null"]) { + gotNull = YES; + XCTAssertTrue([data val] == [NSNull null], @"got null"); + } else { + XCTFail(@"unknown tree"); + } + }]; + + XCTAssertTrue(got1 && got2 && got3 && gotNull, @"saw all the children"); + XCTAssertTrue(num == 4, @"Saw the right number of children"); +} + +- (void) testSetLeafAndForgetDeeperPath { + FSparseSnapshotTree* st = [[FSparseSnapshotTree alloc] init]; + [st rememberData:NODE(@"bar") onPath:PATH(@"foo")]; + BOOL safeToRemove = [st forgetPath:PATH(@"foo/baz")]; + XCTAssertFalse(safeToRemove, @"Should not have deleted anything, nothing to remove"); +} + +@end diff --git a/Example/Database/Tests/Unit/FSyncPointTests.h b/Example/Database/Tests/Unit/FSyncPointTests.h new file mode 100644 index 0000000..bc010ae --- /dev/null +++ b/Example/Database/Tests/Unit/FSyncPointTests.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "FTestBase.h" + +@interface FSyncPointTests : FTestBase + +@end diff --git a/Example/Database/Tests/Unit/FSyncPointTests.m b/Example/Database/Tests/Unit/FSyncPointTests.m new file mode 100644 index 0000000..d36b48a --- /dev/null +++ b/Example/Database/Tests/Unit/FSyncPointTests.m @@ -0,0 +1,905 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <Foundation/Foundation.h> +#import "FSyncPointTests.h" +#import "FListenProvider.h" +#import "FQuerySpec.h" +#import "FQueryParams.h" +#import "FPathIndex.h" +#import "FKeyIndex.h" +#import "FPriorityIndex.h" +#import "FIRDatabaseQuery_Private.h" +#import "FSyncTree.h" +#import "FChange.h" +#import "FDataEvent.h" +#import "FIRDataSnapshot_Private.h" +#import "FCancelEvent.h" +#import "FSnapshotUtilities.h" +#import "FEventRegistration.h" +#import "FCompoundWrite.h" +#import "FEmptyNode.h" +#import "FTestClock.h" +#import "FIRDatabaseConfig_Private.h" +#import "FSnapshotUtilities.h" + +typedef NSDictionary* (^fbt_nsdictionary_void)(void); + +@interface FTestEventRegistration : NSObject<FEventRegistration> +@property (nonatomic, strong) NSDictionary *spec; +@property (nonatomic, strong) FQuerySpec *query; +@end + +@implementation FTestEventRegistration +- (id) initWithSpec:(NSDictionary *)eventSpec query:(FQuerySpec *)query { + self = [super init]; + if (self) { + self.spec = eventSpec; + self.query = query; + } + return self; +} + +- (BOOL) responseTo:(FIRDataEventType)eventType { + return YES; +} +- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query { + FIRDataSnapshot *snap = nil; + FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:nil path:query.path]; + if (change.type == FIRDataEventTypeValue) { + snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode]; + } else { + snap = [[FIRDataSnapshot alloc] initWithRef:[ref child:change.childKey] + indexedNode:change.indexedNode]; + } + return [[FDataEvent alloc] initWithEventType:change.type eventRegistration:self dataSnapshot:snap prevName:change.prevKey]; +} + +- (BOOL) matches:(id<FEventRegistration>)other { + if (![other isKindOfClass:[FTestEventRegistration class]]) { + return NO; + } else { + FTestEventRegistration *otherRegistration = other; + if (self.spec[@"callbackId"] && otherRegistration.spec[@"callbackId"] && + [self.spec[@"callbackId"] isEqualToNumber:otherRegistration.spec[@"callbackId"]]) { + return YES; + } else { + return NO; + } + } +} + +- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue { + [NSException raise:@"NotImplementedError" format:@"Method not implemneted."]; +} +- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path { + [NSException raise:@"NotImplementedError" format:@"Method not implemneted."]; + return nil; +} + +- (FIRDatabaseHandle) handle { + [NSException raise:@"NotImplementedError" format:@"Method not implemneted."]; + return 0; +} +@end + +@implementation FSyncPointTests + +- (NSString *) queryKeyForQuery:(FQuerySpec *)query tagId:(NSNumber *)tagId { + return [NSString stringWithFormat:@"%@|%@|%@", query.path, query.params, tagId]; +} + +- (void) actualEvent:(FDataEvent *)actual equalsExpected:(NSDictionary *)expected { + XCTAssertEqual(actual.eventType, [self stringToEventType:expected[@"type"]], @"Event type should be equal"); + if (actual.eventType != FIRDataEventTypeValue) { + NSString *childName = actual.snapshot.key; + XCTAssertEqualObjects(childName, expected[@"name"], @"Snapshot name should be equal"); + if (expected[@"prevName"] == [NSNull null]) { + XCTAssertNil(actual.prevName, @"prevName should be nil"); + } else { + XCTAssertEqualObjects(actual.prevName, expected[@"prevName"], @"prevName should be equal"); + } + } + NSString *actualHash = [actual.snapshot.node.node dataHash]; + NSString *expectedHash = [[FSnapshotUtilities nodeFrom:expected[@"data"]] dataHash]; + XCTAssertEqualObjects(actualHash, expectedHash, @"Data hash should be equal"); +} + +/** +* @param actual is an array of id<FEvent> +* @param expected is an array of dictionaries? +*/ +- (void) actualEvents:(NSArray *)actual exactMatchesExpected:(NSArray *)expected { + if ([expected count] < [actual count]) { + XCTFail(@"Got extra events: %@", actual); + } else if ([expected count] > [actual count]) { + XCTFail(@"Missing events: %@", actual); + } else { + NSUInteger i = 0; + for (i = 0; i < [expected count]; i++) { + FDataEvent *actualEvent = actual[i]; + NSDictionary *expectedEvent = expected[i]; + [self actualEvent:actualEvent equalsExpected:expectedEvent]; + } + } +} + +- (void)assertOrderedFirstEvent:(FIRDataEventType)e1 secondEvent:(FIRDataEventType)e2 { + static NSArray *eventOrdering = nil; + if (!eventOrdering) { + eventOrdering = @[ + [NSNumber numberWithInteger:FIRDataEventTypeChildRemoved], + [NSNumber numberWithInteger:FIRDataEventTypeChildAdded], + [NSNumber numberWithInteger:FIRDataEventTypeChildMoved], + [NSNumber numberWithInteger:FIRDataEventTypeChildChanged], + [NSNumber numberWithInteger:FIRDataEventTypeValue] + ]; + } + NSUInteger idx1 = [eventOrdering indexOfObject:[NSNumber numberWithInteger:e1]]; + NSUInteger idx2 = [eventOrdering indexOfObject:[NSNumber numberWithInteger:e2]]; + if (idx1 > idx2) { + XCTFail(@"Received %d after %d", (int)e2, (int)e1); + } +} + +- (FIRDataEventType)stringToEventType:(NSString *)stringType { + if ([stringType isEqualToString:@"child_added"]) { + return FIRDataEventTypeChildAdded; + } else if ([stringType isEqualToString:@"child_removed"]) { + return FIRDataEventTypeChildRemoved; + } else if ([stringType isEqualToString:@"child_changed"]) { + return FIRDataEventTypeChildChanged; + } else if ([stringType isEqualToString:@"child_moved"]) { + return FIRDataEventTypeChildMoved; + } else if ([stringType isEqualToString:@"value"]) { + return FIRDataEventTypeValue; + } else { + XCTFail(@"Unknown event type %@", stringType); + return FIRDataEventTypeValue; + } +} + +- (void) actualEventSet:(id)actual matchesExpected:(id)expected atBasePath:(NSString *)basePathStr { + // don't worry about order for now + XCTAssertEqual([expected count], [actual count], @"Mismatched lengths.\nExpected: %@\nActual: %@", expected, actual); + + NSArray *currentExpected = expected; + NSArray *currentActual = actual; + FPath *basePath = basePathStr != nil ? [[FPath alloc] initWith:basePathStr] : [FPath empty]; + while ([currentExpected count] > 0) { + // Step 1: find location range in expected + // we expect all events for a particular path to be in a group + FPath *currentPath = [basePath childFromString:currentExpected[0][@"path"]]; + NSUInteger i = 1; + while (i < [currentExpected count]) { + FPath *otherPath = [basePath childFromString:currentExpected[i][@"path"]]; + if ([currentPath isEqual:otherPath]) { + i++; + } else { + break; + } + } + + // Step 2: foreach in actual, asserting location + NSUInteger j = 0; + for (j = 0; j < i; j++) { + FDataEvent *actualEventData = currentActual[j]; + FTestEventRegistration *eventRegistration = actualEventData.eventRegistration; + NSDictionary *specStep = eventRegistration.spec; + FPath *actualPath = [basePath childFromString:specStep[@"path"]]; + if (![currentPath isEqual:actualPath]) { + XCTFail(@"Expected path %@ to equal %@", actualPath, currentPath); + } + } + + // Step 3: slice each array + NSMutableArray *expectedSlice = [[currentExpected subarrayWithRange:NSMakeRange(0, i)] mutableCopy]; + NSArray *actualSlice = [currentActual subarrayWithRange:NSMakeRange(0, i)]; + + // foreach in actual, stack up to enforce ordering, find in expected + NSMutableDictionary *actualMap = [[NSMutableDictionary alloc] init]; + for (FDataEvent *actualEvent in actualSlice) { + FTestEventRegistration *eventRegistration = actualEvent.eventRegistration; + FQuerySpec *query = eventRegistration.query; + NSDictionary *spec = eventRegistration.spec; + NSString *listenId = [NSString stringWithFormat:@"%@|%@", [basePath childFromString:spec[@"path"]], query]; + if (actualMap[listenId]) { + // stack this event up, and make sure it obeys ordering constraints + NSMutableArray *eventStack = actualMap[listenId]; + FDataEvent *prevEvent = eventStack[[eventStack count] - 1]; + [self assertOrderedFirstEvent:prevEvent.eventType secondEvent:actualEvent.eventType]; + [eventStack addObject:actualEvent]; + } else { + // this is the first event for this listen, just initialize it + actualMap[listenId] = [[NSMutableArray alloc] initWithObjects:actualEvent, nil]; + } + // Ordering has been enforced, make sure we can find this in the expected events + __block NSUInteger indexToRemove = NSNotFound; + [expectedSlice enumerateObjectsUsingBlock:^(NSDictionary *expectedEvent, NSUInteger idx, BOOL *stop) { + if ([self stringToEventType:expectedEvent[@"type"]] == actualEvent.eventType) { + if ([self stringToEventType:expectedEvent[@"type"]] != FIRDataEventTypeValue) { + if (![expectedEvent[@"name"] isEqualToString:actualEvent.snapshot.key]) { + return; // short circuit, not a match + } + if ([self stringToEventType:expectedEvent[@"type"]] != FIRDataEventTypeChildRemoved && + !(expectedEvent[@"prevName"] == [NSNull null] && actualEvent.prevName == nil) && + !(expectedEvent[@"prevName"] != [NSNull null] && [expectedEvent[@"prevName"] isEqualToString:actualEvent.prevName])) { + return; // short circuit, not a match + } + } + // make sure the snapshots match + NSString *snapHash = [actualEvent.snapshot.node.node dataHash]; + NSString *expectedHash = [[FSnapshotUtilities nodeFrom:expectedEvent[@"data"]] dataHash]; + if ([snapHash isEqualToString:expectedHash]) { + indexToRemove = idx; + *stop = YES; + } + } + }]; + XCTAssertFalse(indexToRemove == NSNotFound, @"Could not find matching expected event for %@", actualEvent); + [expectedSlice removeObjectAtIndex:indexToRemove]; + } + currentExpected = [currentExpected subarrayWithRange:NSMakeRange(i, [currentExpected count] - i)]; + currentActual = [currentActual subarrayWithRange:NSMakeRange(i, [currentActual count] - i)]; + } +} + +- (FQuerySpec *)parseParams:(NSDictionary *)specParams forPath:(FPath *)path { + FQueryParams *query = [[FQueryParams alloc] init]; + NSMutableDictionary *params; + + if (specParams) { + params = [specParams mutableCopy]; + if (!params[@"tag"]) { + XCTFail(@"Error: Non-default queries must have tag"); + } + } else { + params = [NSMutableDictionary dictionary]; + } + + if (params[@"orderBy"]) { + FPath *indexPath = [FPath pathWithString:params[@"orderBy"]]; + id<FIndex> index = [[FPathIndex alloc] initWithPath:indexPath]; + query = [query orderBy:index]; + [params removeObjectForKey:@"orderBy"]; + } + if (params[@"orderByKey"]) { + query = [query orderBy:[FKeyIndex keyIndex]]; + [params removeObjectForKey:@"orderByKey"]; + } + if (params[@"orderByPriority"]) { + query = [query orderBy:[FPriorityIndex priorityIndex]]; + [params removeObjectForKey:@"orderByPriority"]; + } + + if (params[@"startAt"]) { + id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"startAt"][@"index"]]; + if (params[@"startAt"][@"name"]) { + query = [query startAt:node childKey:params[@"startAt"][@"name"]]; + } else { + query = [query startAt:node]; + } + [params removeObjectForKey:@"startAt"]; + } + if (params[@"endAt"]) { + id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"endAt"][@"index"]]; + if (params[@"endAt"][@"name"]) { + query = [query endAt:node childKey:params[@"endAt"][@"name"]]; + } else { + query = [query endAt:node]; + } + [params removeObjectForKey:@"endAt"]; + } + if (params[@"equalTo"]) { + id<FNode> node = [FSnapshotUtilities nodeFrom:params[@"equalTo"][@"index"]]; + if (params[@"equalTo"][@"name"]) { + NSString *name = params[@"equalTo"][@"name"]; + query = [[query startAt:node childKey:name] endAt:node childKey:name]; + } else { + query = [[query startAt:node] endAt:node]; + } + [params removeObjectForKey:@"equalTo"]; + } + + if (params[@"limitToFirst"]) { + query = [query limitToFirst:[params[@"limitToFirst"] integerValue]]; + [params removeObjectForKey:@"limitToFirst"]; + } + if (params[@"limitToLast"]) { + query = [query limitToLast:[params[@"limitToLast"] integerValue]]; + [params removeObjectForKey:@"limitToLast"]; + } + + [params removeObjectForKey:@"tag"]; + if ([params count] > 0) { + XCTFail(@"Unsupported query parameter: %@", params); + } + return [[FQuerySpec alloc] initWithPath:path params:query]; +} + +- (void) runTest:(NSDictionary *)testSpec atBasePath:(NSString *)basePath { + NSMutableDictionary *listens = [[NSMutableDictionary alloc] init]; + __weak FSyncPointTests *weakSelf = self; + + FListenProvider *listenProvider = [[FListenProvider alloc] init]; + listenProvider.startListening = ^(FQuerySpec *query, NSNumber *tagId, id<FSyncTreeHash> hash, fbt_nsarray_nsstring onComplete) { + FQueryParams *queryParams = query.params; + FPath *path = query.path; + NSString *logTag = [NSString stringWithFormat:@"%@ (%@)", queryParams, tagId]; + NSString *key = [weakSelf queryKeyForQuery:query tagId:tagId]; + FFLog(@"I-RDB143001", @"Listening at %@ for %@", path, logTag); + id existing = listens[key]; + NSAssert(existing == nil, @"Duplicate listen"); + listens[key] = @YES; + return @[]; + }; + + listenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tagId) { + FQueryParams *queryParams = query.params; + FPath *path = query.path; + NSString *logTag = [NSString stringWithFormat:@"%@ (%@)", queryParams, tagId]; + NSString *key = [weakSelf queryKeyForQuery:query tagId:tagId]; + FFLog(@"I-RDB143002", @"Stop listening at %@ for %@", path, logTag); + id existing = listens[key]; + XCTAssertTrue(existing != nil, @"Missing record of query that we're removing"); + [listens removeObjectForKey:key]; + }; + + FSyncTree *syncTree = [[FSyncTree alloc] initWithListenProvider:listenProvider]; + + NSLog(@"Running %@", testSpec[@"name"]); + NSInteger currentWriteId = 0; + for (NSDictionary *step in testSpec[@"steps"]) { + NSMutableDictionary *spec = [step mutableCopy]; + if (spec[@".comment"]) { + NSLog(@" > %@", spec[@".comment"]); + } + if (spec[@"debug"] != nil) { + // TODO: Ideally we'd pause the debugger somehow (like "debugger;" in JS). + NSLog(@"Start debugging"); + } + // Almost everything has a path... + FPath *path = [FPath empty]; + if (basePath != nil) { + path = [path childFromString:basePath]; + } + if (spec[@"path"] != nil) { + path = [path childFromString:spec[@"path"]]; + } + NSArray *events; + if ([spec[@"type"] isEqualToString:@"listen"]) { + FQuerySpec *query = [self parseParams:spec[@"params"] forPath:path]; + FTestEventRegistration *eventRegistration = [[FTestEventRegistration alloc] initWithSpec:spec query:query]; + events = [syncTree addEventRegistration:eventRegistration forQuery:query]; + [self actualEvents:events exactMatchesExpected:spec[@"events"]]; + + } else if ([spec[@"type"] isEqualToString:@"unlisten"]) { + FQuerySpec *query = [self parseParams:spec[@"params"] forPath:path]; + FTestEventRegistration *eventRegistration = [[FTestEventRegistration alloc] initWithSpec:spec query:query]; + events = [syncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil]; + [self actualEvents:events exactMatchesExpected:spec[@"events"]]; + + } else if ([spec[@"type"] isEqualToString:@"serverUpdate"]) { + id<FNode> update = [FSnapshotUtilities nodeFrom:spec[@"data"]]; + if (spec[@"tag"]) { + events = [syncTree applyTaggedQueryOverwriteAtPath:path newData:update tagId:spec[@"tag"]]; + } else { + events = [syncTree applyServerOverwriteAtPath:path newData:update]; + } + [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath]; + + } else if ([spec[@"type"] isEqualToString:@"serverMerge"]) { + FCompoundWrite *compoundWrite = [FCompoundWrite compoundWriteWithValueDictionary:spec[@"data"]]; + if (spec[@"tag"]) { + events = [syncTree applyTaggedQueryMergeAtPath:path changedChildren:compoundWrite tagId:spec[@"tag"]]; + } else { + events = [syncTree applyServerMergeAtPath:path changedChildren:compoundWrite]; + } + [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath]; + + } else if ([spec[@"type"] isEqualToString:@"set"]) { + id<FNode> toSet = [FSnapshotUtilities nodeFrom:spec[@"data"]]; + BOOL visible = (spec[@"visible"] != nil) ? [spec[@"visible"] boolValue] : YES; + events = [syncTree applyUserOverwriteAtPath:path newData:toSet writeId:currentWriteId++ isVisible:visible]; + [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath]; + + } else if ([spec[@"type"] isEqualToString:@"update"]) { + FCompoundWrite *compoundWrite = [FCompoundWrite compoundWriteWithValueDictionary:spec[@"data"]]; + events = [syncTree applyUserMergeAtPath:path changedChildren:compoundWrite writeId:currentWriteId++]; + [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath]; + } else if ([spec[@"type"] isEqualToString:@"ackUserWrite"]) { + NSInteger writeId = [spec[@"writeId"] integerValue]; + BOOL revert = [spec[@"revert"] boolValue]; + events = [syncTree ackUserWriteWithWriteId:writeId revert:revert persist:YES clock:[[FTestClock alloc] init]]; + [self actualEventSet:events matchesExpected:spec[@"events"] atBasePath:basePath]; + } else if ([spec[@"type"] isEqualToString:@"suppressWarning"]) { + // Do nothing. This is a hack so JS's Jasmine tests don't throw warnings for "expect no errors" tests. + } else { + XCTFail(@"Unknown step: %@", spec[@"type"]); + } + } +} + +- (NSArray *) loadSpecs { + static NSArray *json; + if (json == nil) { + NSString *syncPointSpec = [[NSBundle bundleForClass:[FSyncPointTests class]] pathForResource:@"syncPointSpec" ofType:@"json"]; + NSLog(@"%@", syncPointSpec); + NSData *specData = [NSData dataWithContentsOfFile:syncPointSpec]; + NSError *error = nil; + json = [NSJSONSerialization JSONObjectWithData:specData options:kNilOptions error:&error]; + + if (error) { + XCTFail(@"Error occurred parsing JSON: %@", error); + } + } + + return json; +} + +- (NSDictionary *) specsForName:(NSString *)name { + for (NSDictionary *spec in [self loadSpecs]) { + if ([name isEqualToString:spec[@"name"]]) { + return spec; + } + } + + XCTFail(@"No such test: %@", name); + return nil; +} + +- (void) runTestForName:(NSString *)name { + NSDictionary *spec = [self specsForName:name]; + [self runTest:spec atBasePath:nil]; + // run again at a deeper location + [self runTest:spec atBasePath:@"/foo/bar/baz"]; +} + +- (void) testAll { + NSArray *specs = [self loadSpecs]; + for (NSDictionary *spec in specs) { + [self runTest:spec atBasePath:nil]; + // run again at a deeper location + [self runTest:spec atBasePath:@"/foo/bar/baz"]; + } +} + +- (void) testDefaultListenHandlesParentSet { + [self runTestForName:@"Default listen handles a parent set"]; +} + +- (void) testDefaultListenHandlesASetAtTheSameLevel { + [self runTestForName:@"Default listen handles a set at the same level"]; +} + +- (void) testAQueryCanGetACompleteCacheThenAMerge { + [self runTestForName:@"A query can get a complete cache then a merge"]; +} + +- (void) testServerMergeOnListenerWithCompleteChildren { + [self runTestForName:@"Server merge on listener with complete children"]; +} + +- (void) testDeepMergeOnListenerWithCompleteChildren { + [self runTestForName:@"Deep merge on listener with complete children"]; +} + +- (void) testUpdateChildListenerTwice { + [self runTestForName:@"Update child listener twice"]; +} + +- (void) testChildOfDefaultListenThatAlreadyHasACompleteCache { + [self runTestForName:@"Update child of default listen that already has a complete cache"]; +} + +- (void) testUpdateChildOfDefaultListenThatHasNoCache { + [self runTestForName:@"Update child of default listen that has no cache"]; +} + +// failing +- (void) testUpdateTheChildOfACoLocatedDefaultListenerAndQuery { + [self runTestForName:@"Update (via set) the child of a co-located default listener and query"]; +} + +- (void) testUpdateTheChildOfAQueryWithAFullCache { + [self runTestForName:@"Update (via set) the child of a query with a full cache"]; +} + +- (void) testUpdateAChildBelowAnEmptyQuery { + [self runTestForName:@"Update (via set) a child below an empty query"]; +} + +- (void) testUpdateDescendantOfDefaultListenerWithFullCache { + [self runTestForName:@"Update descendant of default listener with full cache"]; +} + +- (void) testDescendantSetBelowAnEmptyDefaultLIstenerIsIgnored { + [self runTestForName:@"Descendant set below an empty default listener is ignored"]; +} + +- (void) testUpdateOfAChild { + [self runTestForName:@"Update of a child. This can happen if a child listener is added and removed"]; +} + +- (void) testRevertSetWithOnlyChildCaches { + [self runTestForName:@"Revert set with only child caches"]; +} + +- (void) testCanRevertADuplicateChildSet { + [self runTestForName:@"Can revert a duplicate child set"]; +} + +- (void) testCanRevertAChildSetAndSeeTheUnderlyingData { + [self runTestForName:@"Can revert a child set and see the underlying data"]; +} + +- (void) testRevertChildSetWithNoServerData { + [self runTestForName:@"Revert child set with no server data"]; +} + +- (void) testRevertDeepSetWithNoServerData { + [self runTestForName:@"Revert deep set with no server data"]; +} + +- (void) testRevertSetCoveredByNonvisibleTransaction { + [self runTestForName:@"Revert set covered by non-visible transaction"]; +} + +- (void) testClearParentShadowingServerValuesSetWithServerChildren { + [self runTestForName:@"Clear parent shadowing server values set with server children"]; +} + +- (void) testClearChildShadowingServerValuesSetWithServerChildren { + [self runTestForName:@"Clear child shadowing server values set with server children"]; +} + +- (void) testUnrelatedMergeDoesntShadowServerUpdates { + [self runTestForName:@"Unrelated merge doesn't shadow server updates"]; +} + +- (void) testCanSetAlongsideARemoteMerge { + [self runTestForName:@"Can set alongside a remote merge"]; +} + +- (void) testSetPriorityOnALocationWithNoCache { + [self runTestForName:@"setPriority on a location with no cache"]; +} + +- (void) testDeepUpdateDeletesChildFromLimitWindowAndPullsInNewChild { + [self runTestForName:@"deep update deletes child from limit window and pulls in new child"]; +} + +- (void) testDeepSetDeletesChildFromLimitWindowAndPullsInNewChild { + [self runTestForName:@"deep set deletes child from limit window and pulls in new child"]; +} + +- (void) testEdgeCaseInNewChildForChange { + [self runTestForName:@"Edge case in newChildForChange_"]; +} + +- (void) testRevertSetInQueryWindow { + [self runTestForName:@"Revert set in query window"]; +} + +- (void) testHandlesAServerValueMovingAChildOutOfAQueryWindow { + [self runTestForName:@"Handles a server value moving a child out of a query window"]; +} + +- (void) testUpdateOfIndexedChildWorks { + [self runTestForName:@"Update of indexed child works"]; +} + +- (void) testMergeAppliedToEmptyLimit { + [self runTestForName:@"Merge applied to empty limit"]; +} + +- (void) testLimitIsRefilledFromServerDataAfterMerge { + [self runTestForName:@"Limit is refilled from server data after merge"]; +} + +- (void) testHandleRepeatedListenWithMergeAsFirstUpdate { + [self runTestForName:@"Handle repeated listen with merge as first update"]; +} + +- (void) testLimitIsRefilledFromServerDataAfterSet { + [self runTestForName:@"Limit is refilled from server data after set"]; +} + +- (void) testQueryOnWeirdPath { + [self runTestForName:@"query on weird path."]; +} + +- (void) testRunsRound2 { + [self runTestForName:@"runs, round2"]; +} + +- (void) testHandlesNestedListens { + [self runTestForName:@"handles nested listens"]; +} + +- (void) testHandlesASetBelowAListen { + [self runTestForName:@"Handles a set below a listen"]; +} + +- (void) testDoesNonDefaultQueries { + [self runTestForName:@"does non-default queries"]; +} + +- (void) testHandlesCoLocatedDefaultListenerAndQuery { + [self runTestForName:@"handles a co-located default listener and query"]; +} + +- (void) testDefaultAndNonDefaultListenerAtSameLocationWithServerUpdate { + [self runTestForName:@"Default and non-default listener at same location with server update"]; +} + +- (void) testAddAParentListenerToACompleteChildListenerExpectChildEvent { + [self runTestForName:@"Add a parent listener to a complete child listener, expect child event"]; +} + +- (void) testAddListensToASetExpectCorrectEventsIncludingAChildEvent { + [self runTestForName:@"Add listens to a set, expect correct events, including a child event"]; +} + +- (void) testServerUpdateToAChildListenerRaisesChildEventsAtParent { + [self runTestForName:@"ServerUpdate to a child listener raises child events at parent"]; +} + +- (void) testServerUpdateToAChildListenerRaisesChildEventsAtParentQuery { + [self runTestForName:@"ServerUpdate to a child listener raises child events at parent query"]; +} + +- (void) testMultipleCompleteChildrenAreHandleProperly { + [self runTestForName:@"Multiple complete children are handled properly"]; +} + +- (void) testWriteLeafNodeOverwriteAtParentNode { + [self runTestForName:@"Write leaf node, overwrite at parent node"]; +} + +- (void) testConfirmCompleteChildrenFromTheServer { + [self runTestForName:@"Confirm complete children from the server"]; +} + +- (void) testWriteLeafOverwriteFromParent { + [self runTestForName:@"Write leaf, overwrite from parent"]; +} + +- (void) testBasicUpdateTest { + [self runTestForName:@"Basic update test"]; +} + +- (void) testNoDoubleValueEventsForUserAck { + [self runTestForName:@"No double value events for user ack"]; +} + +- (void) testBasicKeyIndexSanityCheck { + [self runTestForName:@"Basic key index sanity check"]; +} + +- (void) testCollectCorrectSubviewsToListenOn { + [self runTestForName:@"Collect correct subviews to listen on"]; +} + +- (void) testLimitToFirstOneOnOrderedQuery { + [self runTestForName:@"Limit to first one on ordered query"]; +} + +- (void) testLimitToLastOneOnOrderedQuery { + [self runTestForName:@"Limit to last one on ordered query"]; +} + +- (void) testUpdateIndexedValueOnExistingChildFromLimitedQuery { + [self runTestForName:@"Update indexed value on existing child from limited query"]; +} + +- (void) testCanCreateStartAtEndAtEqualToQueriesWithBool { + [self runTestForName:@"Can create startAt, endAt, equalTo queries with bool"]; +} + +- (void) testQueryWithExistingServerSnap { + [self runTestForName:@"Query with existing server snap"]; +} + +- (void) testServerDataIsNotPurgedForNonServerIndexedQueries { + [self runTestForName:@"Server data is not purged for non-server-indexed queries"]; +} + +- (void) testStartAtEndAtDominatesLimit { + [self runTestForName:@"startAt/endAt dominates limit"]; +} + +- (void) testUpdateToSingleChildThatMovesOutOfWindow { + [self runTestForName:@"Update to single child that moves out of window"]; +} + +- (void) testLimitedQueryDoesntPullInOutOfRangeChild { + [self runTestForName:@"Limited query doesn't pull in out of range child"]; +} + +- (void) testWithCustomOrderByIsRefilledWithCorrectItem { + [self runTestForName:@"Limit with custom orderBy is refilled with correct item"]; +} + +- (void) testMergeForLocationWithDefaultAndLimitedListener { + [self runTestForName:@"Merge for location with default and limited listener"]; +} + +- (void) testUserMergePullsInCorrectValues { + [self runTestForName:@"User merge pulls in correct values"]; +} + +- (void) testUserDeepSetPullsInCorrectValues { + [self runTestForName:@"User deep set pulls in correct values"]; +} + +- (void) testQueriesWithEqualToNullWork { + [self runTestForName:@"Queries with equalTo(null) work"]; +} + +- (void) testRevertedWritesUpdateQuery { + [self runTestForName:@"Reverted writes update query"]; +} + +- (void) testDeepSetForNonLocalDataDoesntRaiseEvents { + [self runTestForName:@"Deep set for non-local data doesn't raise events"]; +} + +- (void) testUserUpdateWithNewChildrenTriggersEvents { + [self runTestForName:@"User update with new children triggers events"]; +} + +- (void) testUserWriteWithDeepOverwrite { + [self runTestForName:@"User write with deep user overwrite"]; +} + +- (void) testServerUpdatesPriority { + [self runTestForName:@"Server updates priority"]; +} + +- (void) testRevertFullUnderlyingWrite { + [self runTestForName:@"Revert underlying full overwrite"]; +} + +- (void) testUserChildOverwriteForNonexistentServerNode { + [self runTestForName:@"User child overwrite for non-existent server node"]; +} + +- (void) testRevertUserOverwriteOfChildOnLeafNode { + [self runTestForName:@"Revert user overwrite of child on leaf node"]; +} + +- (void) testServerOverwriteWithDeepUserDelete { + [self runTestForName:@"Server overwrite with deep user delete"]; +} + +- (void) testUserOverwritesLeafNodeWithPriority { + [self runTestForName:@"User overwrites leaf node with priority"]; +} + +- (void) testUserOverwritesInheritPriorityValuesFromLeafNodes { + [self runTestForName:@"User overwrites inherit priority values from leaf nodes"]; +} + +- (void) testUserUpdateOnUserSetLeafNodeWithPriorityAfterServerUpdate { + [self runTestForName:@"User update on user set leaf node with priority after server update"]; +} + +- (void) testServerDeepDeleteOnLeafNode { + [self runTestForName:@"Server deep delete on leaf node"]; +} + +- (void) testUserSetsRootPriority { + [self runTestForName:@"User sets root priority"]; +} + +- (void) testUserUpdatesPriorityOnEmptyRoot { + [self runTestForName:@"User updates priority on empty root"]; +} + +- (void) testRevertSetAtRootWithPriority { + [self runTestForName:@"Revert set at root with priority"]; +} + +- (void) testServerUpdatesPriorityAfterUserSetsPriority { + [self runTestForName:@"Server updates priority after user sets priority"]; +} + +- (void) testEmptySetDoesntPreventServerUpdates { + [self runTestForName:@"Empty set doesn't prevent server updates"]; +} + +- (void) testUserUpdatesPriorityTwiceFirstIsReverted { + [self runTestForName:@"User updates priority twice, first is reverted"]; +} + +- (void) testServerAcksRootPrioritySetAfterUserDeletesRootNode { + [self runTestForName:@"Server acks root priority set after user deletes root node"]; +} + +- (void) testADeleteInAMergeDoesntPushOutNodes { + [self runTestForName:@"A delete in a merge doesn't push out nodes"]; +} + +- (void) testATaggedQueryFiresEventsEventually { + [self runTestForName:@"A tagged query fires events eventually"]; +} + +- (void) testUserWriteOutsideOfLimitIsIgnoredForTaggedQueries { + [self runTestForName:@"User write outside of limit is ignored for tagged queries"]; +} + +- (void) testAckForMergeDoesntRaiseValueEventForLaterListen { + [self runTestForName:@"Ack for merge doesn't raise value event for later listen"]; +} + +- (void) testClearParentShadowingServerValuesMergeWithServerChildren { + [self runTestForName:@"Clear parent shadowing server values merge with server children"]; +} + +- (void) testPrioritiesDontMakeMeSick { + [self runTestForName:@"Priorities don't make me sick"]; +} + +- (void) testMergeThatMovesChildFromWindowToBoundaryDoesNotCauseChildToBeReadded { + [self runTestForName:@"Merge that moves child from window to boundary does not cause child to be readded"]; +} + +- (void) testDeepMergeAckIsHandledCorrectly { + [self runTestForName:@"Deep merge ack is handled correctly."]; +} + +- (void) testDeepMergeAckOnIncompleteDataAndWithServerValues { + [self runTestForName:@"Deep merge ack (on incomplete data, and with server values)"]; +} + +- (void) testLimitQueryHandlesDeepServerMergeForOutOfViewItem { + [self runTestForName:@"Limit query handles deep server merge for out-of-view item."]; +} + +- (void) testLimitQueryHandlesDeepUserMergeForOutOfViewItem { + [self runTestForName:@"Limit query handles deep user merge for out-of-view item."]; +} + +- (void) testLimitQueryHandlesDeepUserMergeForOutOfViewItemFollowedByServerUpdate { + [self runTestForName:@"Limit query handles deep user merge for out-of-view item followed by server update."]; +} + +- (void) testUnrelatedUntaggedUpdateIsNotCachedInTaggedListen { + [self runTestForName:@"Unrelated, untagged update is not cached in tagged listen"]; +} + +- (void) testUnrelatedAckedSetIsNotCachedInTaggedListen { + [self runTestForName:@"Unrelated, acked set is not cached in tagged listen"]; +} + +- (void) testUnrelatedAckedUpdateIsNotCachedInTaggedListen { + [self runTestForName:@"Unrelated, acked update is not cached in tagged listen"]; +} + +- (void) testdeepUpdateRaisesImmediateEventsOnlyIfHasCompleteData { + [self runTestForName:@"Deep update raises immediate events only if has complete data"]; +} + +- (void) testdeepUpdateReturnsMinimumDataRequired { + [self runTestForName:@"Deep update returns minimum data required"]; +} + +- (void) testdeepUpdateRaisesAllEvents { + [self runTestForName:@"Deep update raises all events"]; +} + +@end diff --git a/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m b/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m new file mode 100644 index 0000000..ebcf9b2 --- /dev/null +++ b/Example/Database/Tests/Unit/FTrackedQueryManagerTest.m @@ -0,0 +1,338 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +#import "FTrackedQueryManager.h" +#import "FTrackedQuery.h" +#import "FMockStorageEngine.h" +#import "FPath.h" +#import "FQuerySpec.h" +#import "FPathIndex.h" +#import "FSnapshotUtilities.h" +#import "FClock.h" +#import "FTestClock.h" +#import "FTestHelpers.h" +#import "FPruneForest.h" +#import "FTestCachePolicy.h" + +@interface FPruneForest (Test) + +- (FImmutableSortedDictionary *)pruneForest; + +@end + +@interface FTrackedQueryManagerTest : XCTestCase + +@end + +@implementation FTrackedQueryManagerTest + +#define SAMPLE_PARAMS \ + ([[[[[FQueryParams defaultInstance] orderBy:[[FPathIndex alloc] initWithPath:PATH(@"child")]] \ + startAt:[FSnapshotUtilities nodeFrom:@"startVal"] childKey:@"startKey"] \ + endAt:[FSnapshotUtilities nodeFrom:@"endVal"] childKey:@"endKey"] \ + limitToLast:5]) + +#define SAMPLE_QUERY \ + ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:SAMPLE_PARAMS]) + +#define DEFAULT_FOO_QUERY \ + ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"foo"] params:[FQueryParams defaultInstance]]) + +#define DEFAULT_BAR_QUERY \ + ([[FQuerySpec alloc] initWithPath:[FPath pathWithString:@"bar"] params:[FQueryParams defaultInstance]]) + +- (FTrackedQueryManager *)newManager { + return [self newManagerWithClock:[FSystemClock clock]]; +} + +- (FTrackedQueryManager *)newManagerWithClock:(id<FClock>)clock { + return [[FTrackedQueryManager alloc] initWithStorageEngine:[[FMockStorageEngine alloc] init] + clock:clock]; +} + +- (FTrackedQueryManager *)newManagerWithStorageEngine:(id<FStorageEngine>)storageEngine { + return [[FTrackedQueryManager alloc] initWithStorageEngine:storageEngine clock:[FSystemClock clock]]; +} + +- (void)testFindTrackedQuery { + FTrackedQueryManager *manager = [self newManager]; + XCTAssertNil([manager findTrackedQuery:SAMPLE_QUERY]); + [manager setQueryActive:SAMPLE_QUERY]; + XCTAssertNotNil([manager findTrackedQuery:SAMPLE_QUERY]); +} + +- (void)testRemoveTrackedQuery { + FTrackedQueryManager *manager = [self newManager]; + [manager setQueryActive:SAMPLE_QUERY]; + XCTAssertNotNil([manager findTrackedQuery:SAMPLE_QUERY]); + [manager removeTrackedQuery:SAMPLE_QUERY]; + XCTAssertNil([manager findTrackedQuery:SAMPLE_QUERY]); + [manager verifyCache]; +} + +- (void)testSetQueryActiveAndInactive { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + [manager setQueryActive:SAMPLE_QUERY]; + FTrackedQuery *q = [manager findTrackedQuery:SAMPLE_QUERY]; + XCTAssertTrue(q.isActive); + XCTAssertEqual(q.lastUse, clock.currentTime); + [manager verifyCache]; + + [clock tick]; + [manager setQueryInactive:SAMPLE_QUERY]; + q = [manager findTrackedQuery:SAMPLE_QUERY]; + XCTAssertFalse(q.isActive); + XCTAssertEqual(q.lastUse, clock.currentTime); + [manager verifyCache]; +} + +- (void)testSetQueryComplete { + FTrackedQueryManager *manager = [self newManager]; + [manager setQueryActive:SAMPLE_QUERY]; + [manager setQueryComplete:SAMPLE_QUERY]; + XCTAssertTrue([manager findTrackedQuery:SAMPLE_QUERY].isComplete); + [manager verifyCache]; +} + +- (void)testSetQueriesComplete { + FTrackedQueryManager *manager = [self newManager]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"elsewhere")]]; + [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:SAMPLE_PARAMS]]; + [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/baz") params:SAMPLE_PARAMS]]; + [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"elsewhere") params:SAMPLE_PARAMS]]; + + [manager setQueriesCompleteAtPath:PATH(@"foo")]; + + XCTAssertTrue([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]].isComplete); + XCTAssertTrue([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"foo/bar")]].isComplete); + XCTAssertTrue([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"foo") params:SAMPLE_PARAMS]].isComplete); + XCTAssertTrue([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"foo/baz") params:SAMPLE_PARAMS]].isComplete); + XCTAssertFalse([manager findTrackedQuery:[FQuerySpec defaultQueryAtPath:PATH(@"elsewhere")]].isComplete); + XCTAssertFalse([manager findTrackedQuery:[[FQuerySpec alloc] initWithPath:PATH(@"elsewhere") params:SAMPLE_PARAMS]].isComplete); + [manager verifyCache]; +} + +- (void)testIsQueryComplete { + FTrackedQueryManager *manager = [self newManager]; + + [manager setQueryActive:SAMPLE_QUERY]; + [manager setQueryComplete:SAMPLE_QUERY]; + + [manager setQueryActive:DEFAULT_BAR_QUERY]; + + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]]; + [manager setQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]]; + + XCTAssertTrue([manager isQueryComplete:SAMPLE_QUERY]); + XCTAssertFalse([manager isQueryComplete:DEFAULT_BAR_QUERY]); + + XCTAssertFalse([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"")]]); + XCTAssertTrue([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz")]]); + XCTAssertTrue([manager isQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"baz/quu")]]); +} + +- (void)testPruneOldQueries { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"active1")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"active2")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"pinned1")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"pinned2")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive1")]]; + [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive1")]]; + [clock tick]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive2")]]; + [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive2")]]; + [clock tick]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive3")]]; + [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive3")]]; + [clock tick]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive4")]]; + [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"inactive4")]]; + [clock tick]; + + // Should remove the first two inactive queries + FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.5 maxQueries:NSUIntegerMax]]; + [self checkPruneForest:forest + pathsToKeep:@[@"active1", @"active2", @"pinned1", @"pinned2", @"inactive3", @"inactive4"] + pathsToPrune:@[@"inactive1", @"inactive2"]]; + + // Should remove the other two inactive queries + forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1 maxQueries:NSUIntegerMax]]; + [self checkPruneForest:forest + pathsToKeep:@[@"active1", @"active2", @"pinned1", @"pinned2"] + pathsToPrune:@[@"inactive3", @"inactive4"]]; + + // Nothing left to prune + forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1 maxQueries:NSUIntegerMax]]; + XCTAssertFalse([forest prunesAnything]); + + [manager verifyCache]; +} + +- (void) testPruneQueriesOverMaxSize { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + for (NSUInteger i = 0; i < 10; i++) { + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]]; + [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]]; + [clock tick]; + } + + FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.2 maxQueries:6]]; + [self checkPruneForest:forest + pathsToKeep:@[@"4", @"5", @"6", @"7", @"8", @"9"] + pathsToPrune:@[@"0", @"1", @"2", @"3"]]; +} + +- (void) testPruneDefaultWithDeeperQueries { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]]; + [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]]; + [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]]; + [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]]; + + FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1.0 maxQueries:NSUIntegerMax]]; + [self checkPruneForest:forest pathsToKeep:@[@"foo/a", @"foo/b"] pathsToPrune:@[@"foo"]]; + [manager verifyCache]; +} + +- (void) testPruneQueriesWithDefaultQueryOnParent { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo")]]; + [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]]; + [manager setQueryActive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]]; + [manager setQueryInactive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/a") params:SAMPLE_PARAMS]]; + [manager setQueryInactive:[[FQuerySpec alloc] initWithPath:PATH(@"foo/b") params:SAMPLE_PARAMS]]; + + FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:1.0 maxQueries:NSUIntegerMax]]; + [self checkPruneForest:forest pathsToKeep:@[@"foo"] pathsToPrune:@[]]; + [manager verifyCache]; +} + +- (void) testPruneQueriesOverMaxSizeUsingPercent { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + for (NSUInteger i = 0; i < 10; i++) { + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]]; + [manager setQueryInactive:[FQuerySpec defaultQueryAtPath:PATH(([NSString stringWithFormat:@"%lu", i]))]]; + [clock tick]; + } + + FPruneForest *forest = [manager pruneOldQueries:[[FTestCachePolicy alloc] initWithPercent:0.6 maxQueries:6]]; + [self checkPruneForest:forest + pathsToKeep:@[@"6", @"7", @"8", @"9"] + pathsToPrune:@[@"0", @"1", @"2", @"3", @"4", @"5"]]; +} + +- (void)checkPruneForest:(FPruneForest *)pruneForest pathsToKeep:(NSArray *)toKeep pathsToPrune:(NSArray *)toPrune { + FPruneForest *checkForest = [FPruneForest empty]; + for (NSString *path in toPrune) { + checkForest = [checkForest prunePath:PATH(path)]; + } + for (NSString *path in toKeep) { + checkForest = [checkForest keepPath:PATH(path)]; + } + XCTAssertEqualObjects([pruneForest pruneForest], [checkForest pruneForest]); +} + +- (void)testKnownCompleteChildren { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithStorageEngine:engine]; + + XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo")], [NSSet set]); + + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/a")]]; + [manager setQueryComplete:[FQuerySpec defaultQueryAtPath:PATH(@"foo/a")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/not-included")]]; + [manager setQueryActive:[FQuerySpec defaultQueryAtPath:PATH(@"foo/deep/not-included")]]; + + [manager setQueryActive:SAMPLE_QUERY]; + FTrackedQuery *query = [manager findTrackedQuery:SAMPLE_QUERY]; + [engine setTrackedQueryKeys:[NSSet setWithArray:@[@"d", @"e"]] forQueryId:query.queryId]; + + XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo")], ([NSSet setWithArray:@[@"a", @"d", @"e"]])); + XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"")], [NSSet set]); + XCTAssertEqualObjects([manager knownCompleteChildrenAtPath:PATH(@"foo/baz")], [NSSet set]); +} + +- (void)testEnsureTrackedQueryForNewQuery { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + [manager ensureCompleteTrackedQueryAtPath:PATH(@"foo")]; + FTrackedQuery *query = [manager findTrackedQuery:DEFAULT_FOO_QUERY]; + XCTAssertTrue(query.isComplete); + XCTAssertEqual(query.lastUse, clock.currentTime); +} + +- (void)testEnsureTrackedQueryForAlreadyTrackedQuery { + FTestClock *clock = [[FTestClock alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithClock:clock]; + + [manager setQueryActive:DEFAULT_FOO_QUERY]; + + NSTimeInterval lastTick = clock.currentTime; + [clock tick]; + [manager ensureCompleteTrackedQueryAtPath:PATH(@"foo")]; + XCTAssertEqual([manager findTrackedQuery:DEFAULT_FOO_QUERY].lastUse, lastTick); +} + +- (void)testHasActiveDefaultQuery { + FTrackedQueryManager *manager = [self newManager]; + + [manager setQueryActive:SAMPLE_QUERY]; + [manager setQueryActive:DEFAULT_BAR_QUERY]; + XCTAssertFalse([manager hasActiveDefaultQueryAtPath:PATH(@"foo")]); + XCTAssertFalse([manager hasActiveDefaultQueryAtPath:PATH(@"")]); + XCTAssertTrue([manager hasActiveDefaultQueryAtPath:PATH(@"bar")]); + XCTAssertTrue([manager hasActiveDefaultQueryAtPath:PATH(@"bar/baz")]); +} + +- (void)testCacheSanity { + FMockStorageEngine *engine = [[FMockStorageEngine alloc] init]; + FTrackedQueryManager *manager = [self newManagerWithStorageEngine:engine]; + + [manager setQueryActive:SAMPLE_QUERY]; + [manager setQueryActive:DEFAULT_FOO_QUERY]; + [manager verifyCache]; + + [manager setQueryComplete:SAMPLE_QUERY]; + [manager verifyCache]; + + [manager setQueryInactive:DEFAULT_FOO_QUERY]; + [manager verifyCache]; + + FTrackedQueryManager *manager2 = [self newManagerWithStorageEngine:engine]; + XCTAssertNotNil([manager2 findTrackedQuery:SAMPLE_QUERY]); + XCTAssertNotNil([manager2 findTrackedQuery:DEFAULT_FOO_QUERY]); + [manager2 verifyCache]; +} + +@end diff --git a/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m b/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m new file mode 100644 index 0000000..6aee84d --- /dev/null +++ b/Example/Database/Tests/Unit/FTreeSortedDictionaryTests.m @@ -0,0 +1,574 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <XCTest/XCTest.h> + +#import "FTreeSortedDictionary.h" +#import "FLLRBNode.h" +#import "FLLRBEmptyNode.h" +#import "FLLRBValueNode.h" + +@interface FLLRBValueNode (Tests) +- (id<FLLRBNode>) rotateLeft; +- (id<FLLRBNode>) rotateRight; +@end + +@interface FTreeSortedDictionaryTests : XCTestCase + +@end + +@implementation FTreeSortedDictionaryTests + +- (NSComparator) defaultComparator { + return ^(id obj1, id obj2) { + if([obj1 respondsToSelector:@selector(compare:)] && [obj2 respondsToSelector:@selector(compare:)]) { + return [obj1 compare:obj2]; + } + else { + if(obj1 < obj2) { + return (NSComparisonResult)NSOrderedAscending; + } + else if (obj1 > obj2) { + return (NSComparisonResult)NSOrderedDescending; + } + else { + return (NSComparisonResult)NSOrderedSame; + } + } + }; +} + +- (void)testCreateNode +{ + FTreeSortedDictionary* map = [[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@"key" withValue:@"value"]; + XCTAssertTrue([map.root.left isEmpty], @"Left child is properly empty"); + XCTAssertTrue([map.root.right isEmpty], @"Right child is properly empty"); +} + +- (void)testGetNilReturnsNil { + FImmutableSortedDictionary *map1 = [[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@"key" withValue:@"value"]; + XCTAssertNil([map1 get:nil]); + + FImmutableSortedDictionary *map2 = [[[FTreeSortedDictionary alloc] initWithComparator:^NSComparisonResult(id obj1, id obj2) { + return [obj1 compare:obj2]; + }] + insertKey:@"key" withValue:@"value"]; + XCTAssertNil([map2 get:nil]); +} + +- (void)testSearchForSpecificKey { + FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2]; + + XCTAssertEqualObjects([map get:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map get:@2], @2, @"Found second object"); + XCTAssertNil([map get:@3], @"Properly not found object"); +} + +- (void)testInsertNewKeyValuePair { + FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2]; + + XCTAssertEqualObjects(map.root.key, @2, @"Check the root key"); + XCTAssertEqualObjects(map.root.left.key, @1, @"Check the root.left key"); +} + +- (void)testRemoveKeyValuePair { + FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2]; + + FImmutableSortedDictionary* newMap = [map removeKey:@1]; + XCTAssertEqualObjects([newMap get:@2], @2, @"Found second object"); + XCTAssertNil([newMap get:@1], @"Properly not found object"); + + // Make sure the original one is not mutated + XCTAssertEqualObjects([map get:@1], @1, @"Found first object"); + XCTAssertEqualObjects([map get:@2], @2, @"Found second object"); +} + +- (void)testMoreRemovals { + FTreeSortedDictionary* map = [[[[[[[[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@50 withValue:@50] + insertKey:@3 withValue:@3] + insertKey:@4 withValue:@4] + insertKey:@7 withValue:@7] + insertKey:@9 withValue:@9] + insertKey:@20 withValue:@20] + insertKey:@18 withValue:@18] + insertKey:@2 withValue:@2] + insertKey:@71 withValue:@71] + insertKey:@42 withValue:@42] + insertKey:@88 withValue:@88]; + XCTAssertNotNil([map get:@7], @"Found object"); + XCTAssertNotNil([map get:@3], @"Found object"); + XCTAssertNotNil([map get:@1], @"Found object"); + + + FImmutableSortedDictionary* m1 = [map removeKey:@7]; + FImmutableSortedDictionary* m2 = [map removeKey:@3]; + FImmutableSortedDictionary* m3 = [map removeKey:@1]; + + XCTAssertNil([m1 get:@7], @"Removed object"); + XCTAssertNotNil([m1 get:@3], @"Found object"); + XCTAssertNotNil([m1 get:@1], @"Found object"); + + XCTAssertNil([m2 get:@3], @"Removed object"); + XCTAssertNotNil([m2 get:@7], @"Found object"); + XCTAssertNotNil([m2 get:@1], @"Found object"); + + + XCTAssertNil([m3 get:@1], @"Removed object"); + XCTAssertNotNil([m3 get:@7], @"Found object"); + XCTAssertNotNil([m3 get:@3], @"Found object"); +} + +- (void) testRemovalBug { + FTreeSortedDictionary* map = [[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2] + insertKey:@3 withValue:@3]; + + XCTAssertEqualObjects([map get:@1], @1, @"Found object"); + XCTAssertEqualObjects([map get:@2], @2, @"Found object"); + XCTAssertEqualObjects([map get:@3], @3, @"Found object"); + + FImmutableSortedDictionary* m1 = [map removeKey:@2]; + XCTAssertEqualObjects([m1 get:@1], @1, @"Found object"); + XCTAssertEqualObjects([m1 get:@3], @3, @"Found object"); + XCTAssertNil([m1 get:@2], @"Removed object"); +} + +- (void) testIncreasing { + int total = 100; + + FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + for(int i = 0; i < total; i++) { + NSNumber* item = [NSNumber numberWithInt:i]; + map = [map insertKey:item withValue:item]; + } + + XCTAssertTrue([map count] == 100, @"Check if all 100 objects are in the map"); + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); + + for(int i = 0; i < total; i++) { + NSNumber* item = [NSNumber numberWithInt:i]; + map = [map removeKey:item]; + } + + XCTAssertTrue([map count] == 0, @"Check if all 100 objects were removed"); + // We can't check the depth here because the map no longer contains values, so we check that it doesn't responsd to this check + XCTAssertTrue([map.root isMemberOfClass:[FLLRBEmptyNode class]], @"Root is an empty node"); + XCTAssertFalse([map respondsToSelector:@selector(checkMaxDepth)], @"The empty node doesn't respond to this selector."); +} + +- (void) testStructureShouldBeValidAfterInsertionA { + FTreeSortedDictionary* map = [[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@2 withValue:@2] + insertKey:@3 withValue:@3]; + + + XCTAssertEqualObjects(map.root.key, @2, @"Check root key"); + XCTAssertEqualObjects(map.root.left.key, @1, @"Check the left key is correct"); + XCTAssertEqualObjects(map.root.right.key, @3, @"Check the right key is correct"); +} + +- (void) testStructureShouldBeValidAfterInsertionB { + FTreeSortedDictionary* map = [[[[[[[[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@50 withValue:@50] + insertKey:@3 withValue:@3] + insertKey:@4 withValue:@4] + insertKey:@7 withValue:@7] + insertKey:@9 withValue:@9] + insertKey:@20 withValue:@20] + insertKey:@18 withValue:@18] + insertKey:@2 withValue:@2] + insertKey:@71 withValue:@71] + insertKey:@42 withValue:@42] + insertKey:@88 withValue:@88]; + + XCTAssertTrue([map count] == 12, @"Check if all 12 objects are in the map"); + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); +} + +- (void) testRotateLeftLeavesTreeInAValidState { + FLLRBValueNode* node = [[FLLRBValueNode alloc] initWithKey:@4 withValue:@4 withColor:BLACK withLeft: + [[FLLRBValueNode alloc] initWithKey:@2 withValue:@2 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc]initWithKey:@7 withValue:@7 withColor:RED withLeft:[[FLLRBValueNode alloc ]initWithKey:@5 withValue:@5 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc] initWithKey:@8 withValue:@8 withColor:BLACK withLeft:nil withRight:nil]]]; + + FLLRBValueNode* node2 = [node performSelector:@selector(rotateLeft)]; + + XCTAssertTrue([node2 count] == 5, @"Make sure the count is correct"); + XCTAssertTrue([node2 checkMaxDepth], @"Check proper structure"); +} + +- (void) testRotateRightLeavesTreeInAValidState { + FLLRBValueNode* node = [[FLLRBValueNode alloc] initWithKey:@7 withValue:@7 withColor:BLACK withLeft:[[FLLRBValueNode alloc] initWithKey:@4 withValue:@4 withColor:RED withLeft:[[FLLRBValueNode alloc] initWithKey:@2 withValue:@2 withColor:BLACK withLeft:nil withRight:nil] withRight:[[FLLRBValueNode alloc] initWithKey:@5 withValue:@5 withColor:BLACK withLeft:nil withRight:nil]] withRight:[[FLLRBValueNode alloc] initWithKey:@8 withValue:@8 withColor:BLACK withLeft:nil withRight:nil]]; + + FLLRBValueNode* node2 = [node performSelector:@selector(rotateRight)]; + + XCTAssertTrue([node2 count] == 5, @"Make sure the count is correct"); + XCTAssertEqualObjects(node2.key, @4, @"Check roots key"); + XCTAssertEqualObjects(node2.left.key, @2, @"Check first left child key"); + XCTAssertEqualObjects(node2.right.key, @7, @"Check first right child key"); + XCTAssertEqualObjects(node2.right.left.key, @5, @"Check second right left key"); + XCTAssertEqualObjects(node2.right.right.key, @8, @"Check second right left key"); +} + +- (void) testStructureShouldBeValidAfterInsertionC { + FTreeSortedDictionary* map = [[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@50 withValue:@50] + insertKey:@3 withValue:@3] + insertKey:@4 withValue:@4] + insertKey:@7 withValue:@7] + insertKey:@9 withValue:@9]; + + XCTAssertTrue([map count] == 6, @"Check if all 6 objects are in the map"); + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); + + FTreeSortedDictionary* m2 = [[[map insertKey:@20 withValue:@20] + insertKey:@18 withValue:@18] + insertKey:@2 withValue:@2]; + XCTAssertTrue([m2 count] == 9, @"Check if all 9 objects are in the map"); + XCTAssertTrue([m2.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)m2.root checkMaxDepth], @"Checking valid depth and tree structure"); + + FTreeSortedDictionary* m3 = [[[[m2 insertKey:@71 withValue:@71] + insertKey:@42 withValue:@42] + insertKey:@88 withValue:@88] + insertKey:@20 withValue:@20]; // Add a dupe to see if the size is correct + XCTAssertTrue([m3 count] == 12, @"Check if all 12 (minus dupe @20) objects are in the map"); + XCTAssertTrue([m3.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)m3.root checkMaxDepth], @"Checking valid depth and tree structure"); +} + +- (void) testOverride { + FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@10 withValue:@10] + insertKey:@10 withValue:@8]; + + XCTAssertEqualObjects([map get:@10], @8, @"Found first object"); +} +- (void) testEmpty { + FTreeSortedDictionary* map = [[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@10 withValue:@10] + removeKey:@10]; + + XCTAssertTrue([map isEmpty], @"Properly empty"); + +} + +- (void) testEmptyGet { + FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertNil([map get:@"something"], @"Properly nil"); +} + +- (void) testEmptyCount { + FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertTrue([map count] == 0, @"Properly zero count"); +} + +- (void) testEmptyRemoval { + FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + XCTAssertTrue([[map removeKey:@"sometjhing"] count] == 0, @"Properly zero count"); +} + +- (void) testReverseTraversal { + FTreeSortedDictionary* map = [[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@5 withValue:@5] + insertKey:@3 withValue:@3] + insertKey:@2 withValue:@2] + insertKey:@4 withValue:@4]; + + __block int next = 5; + [map enumerateKeysAndObjectsReverse:YES usingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Properly equal"); + next = next - 1; + }]; +} + + +- (void) testInsertionAndRemovalOfAHundredItems { + int N = 100; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i]]; + [toRemove addObject:[NSNumber numberWithInt:i]]; + } + + + [self shuffleArray:toInsert]; + [self shuffleArray:toRemove]; + + FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + + // check the order is correct + __block int next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key"); + XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual(next, N, @"Check we traversed all of the items"); + + // remove them + + for(int i = 0; i < N; i++) { + if([map.root isMemberOfClass:[FLLRBValueNode class]]) { + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); + } + map = [map removeKey:[toRemove objectAtIndex:i]]; + } + + + XCTAssertEqual([map count], 0, @"Check we removed all of the items"); +} + +- (void) shuffleArray:(NSMutableArray *)array { + NSUInteger count = [array count]; + for(NSUInteger i = 0; i < count; i++) { + NSInteger nElements = count - i; + NSInteger n = (arc4random() % nElements) + i; + [array exchangeObjectAtIndex:i withObjectAtIndex:n]; + } +} + +- (void) testBalanceProblem { + + NSArray* toInsert = [[NSArray alloc] initWithObjects:@1,@7,@8,@5,@2,@6,@4,@0,@3, nil]; + + FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < [toInsert count]; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); + } + XCTAssertTrue([map count] == [toInsert count], @"Check if all N objects are in the map"); + + // check the order is correct + __block int next = 0; + [map enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + XCTAssertEqualObjects(key, [NSNumber numberWithInt:next], @"Correct key"); + XCTAssertEqualObjects(value, [NSNumber numberWithInt:next], @"Correct value"); + next = next + 1; + }]; + XCTAssertEqual(next, [[NSNumber numberWithUnsignedInteger:[toInsert count]] intValue], @"Check we traversed all of the items"); + + // removing one triggers the balance problem + + map = [map removeKey:@5]; + + if([map.root isMemberOfClass:[FLLRBValueNode class]]) { + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); + } +} + +- (void) testPredecessorKey { + FTreeSortedDictionary* map = [[[[[[[[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]] + insertKey:@1 withValue:@1] + insertKey:@50 withValue:@50] + insertKey:@3 withValue:@3] + insertKey:@4 withValue:@4] + insertKey:@7 withValue:@7] + insertKey:@9 withValue:@9]; + + XCTAssertNil([map getPredecessorKey:@1], @"First object doesn't have a predecessor"); + XCTAssertEqualObjects([map getPredecessorKey:@3], @1, @"@1"); + XCTAssertEqualObjects([map getPredecessorKey:@4], @3, @"@3"); + XCTAssertEqualObjects([map getPredecessorKey:@7], @4, @"@4"); + XCTAssertEqualObjects([map getPredecessorKey:@9], @7, @"@7"); + XCTAssertEqualObjects([map getPredecessorKey:@50], @9, @"@9"); + XCTAssertThrows([map getPredecessorKey:@777], @"Expect exception about nonexistant key"); +} + +- (void) testEnumerator { + int N = 100; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + NSMutableArray* toRemove = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i]]; + [toRemove addObject:[NSNumber numberWithInt:i]]; + } + + + [self shuffleArray:toInsert]; + [self shuffleArray:toRemove]; + + FTreeSortedDictionary* map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + XCTAssertTrue([map.root isMemberOfClass:[FLLRBValueNode class]], @"Root is a value node"); + XCTAssertTrue([(FLLRBValueNode *)map.root checkMaxDepth], @"Checking valid depth and tree structure"); + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + + NSEnumerator* enumerator = [map keyEnumerator]; + id next = [enumerator nextObject]; + int correctValue = 0; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue + 1; + } +} + +- (void) testReverseEnumerator { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary"); + + NSEnumerator* enumerator = [map reverseKeyEnumerator]; + id next = [enumerator nextObject]; + int correctValue = N - 1; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue--; + } +} + +- (void) testEnumeratorFrom { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i*2]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary"); + + // Test from inbetween keys + { + NSEnumerator* enumerator = [map keyEnumeratorFrom:@11]; + id next = [enumerator nextObject]; + int correctValue = 12; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue + 2; + } + } + + // Test from key in map + { + NSEnumerator* enumerator = [map keyEnumeratorFrom:@10]; + id next = [enumerator nextObject]; + int correctValue = 10; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue + 2; + } + } +} + +- (void) testReverseEnumeratorFrom { + int N = 20; + NSMutableArray* toInsert = [[NSMutableArray alloc] initWithCapacity:N]; + + for(int i = 0; i < N; i++) { + [toInsert addObject:[NSNumber numberWithInt:i*2]]; + } + + [self shuffleArray:toInsert]; + + FImmutableSortedDictionary *map = [[FTreeSortedDictionary alloc] initWithComparator:[self defaultComparator]]; + + // add them to the dictionary + for(int i = 0; i < N; i++) { + map = [map insertKey:[toInsert objectAtIndex:i] withValue:[toInsert objectAtIndex:i]]; + } + XCTAssertTrue([map count] == N, @"Check if all N objects are in the map"); + XCTAssertTrue([map isKindOfClass:[FTreeSortedDictionary class]], @"Make sure we still have a array backed dictionary"); + + // Test from inbetween keys + { + NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@11]; + id next = [enumerator nextObject]; + int correctValue = 10; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue - 2; + } + } + + // Test from key in map + { + NSEnumerator* enumerator = [map reverseKeyEnumeratorFrom:@10]; + id next = [enumerator nextObject]; + int correctValue = 10; + while(next != nil) { + XCTAssertEqualObjects(next, [NSNumber numberWithInt:correctValue], @"Correct key"); + next = [enumerator nextObject]; + correctValue = correctValue - 2; + } + } +} + +@end diff --git a/Example/Database/Tests/Unit/FUtilitiesTest.m b/Example/Database/Tests/Unit/FUtilitiesTest.m new file mode 100644 index 0000000..a012250 --- /dev/null +++ b/Example/Database/Tests/Unit/FUtilitiesTest.m @@ -0,0 +1,116 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import <UIKit/UIKit.h> +#import <XCTest/XCTest.h> +#import "FUtilities.h" +#import "FIRDatabase_Private.h" +#import "FIRDatabaseReference_Private.h" +#import "FClock.h" +#import "FIRDatabaseConfig_Private.h" +#import "FWebSocketConnection.h" +#import "FConstants.h" + +@interface FWebSocketConnection (Tests) +- (NSString*)userAgent; +@end + +@interface FUtilitiesTest : XCTestCase + +@end + +@implementation FUtilitiesTest + +- (void)testUrlWithSchema { + FParsedUrl *parsedUrl = [FUtilities parseUrl:@"https://repo.firebaseio.com"]; + XCTAssertEqualObjects(parsedUrl.repoInfo.host, @"repo.firebaseio.com"); + XCTAssertEqualObjects(parsedUrl.repoInfo.namespace, @"repo"); + XCTAssertTrue(parsedUrl.repoInfo.secure); + XCTAssertEqualObjects(parsedUrl.path, [FPath empty]); +} + +- (void)testUrlParsedWithoutSchema { + FParsedUrl *parsedUrl = [FUtilities parseUrl:@"repo.firebaseio.com"]; + XCTAssertEqualObjects(parsedUrl.repoInfo.host, @"repo.firebaseio.com"); + XCTAssertEqualObjects(parsedUrl.repoInfo.namespace, @"repo"); + XCTAssertTrue(parsedUrl.repoInfo.secure); + XCTAssertEqualObjects(parsedUrl.path, [FPath empty]); +} + +- (void)testDefaultCacheSizeIs10MB { + XCTAssertEqual([FIRDatabaseReference defaultConfig].persistenceCacheSizeBytes, (NSUInteger)10*1024*1024); + XCTAssertEqual([FIRDatabaseConfig configForName:@"test-config"].persistenceCacheSizeBytes, (NSUInteger)10*1024*1024); +} + +- (void)testSettingCacheSizeToHighOrToLowThrows { + FIRDatabaseConfig *config = [FIRDatabaseConfig configForName:@"config-tests-config"]; + config.persistenceCacheSizeBytes = 5*1024*1024; // Works fine + XCTAssertThrows(config.persistenceCacheSizeBytes = (1024*1024-1)); + XCTAssertThrows(config.persistenceCacheSizeBytes = 100*1024*1024+1); +} + +- (void)testSystemClockMatchesCurrentTime { + NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; + // Accuracy within 10ms + XCTAssertEqualWithAccuracy(currentTime, [[FSystemClock clock] currentTime], 0.010); +} + +// This test is here for a lack of a better place to put it +- (void)testUserAgentString { + FWebSocketConnection *conn = [[FWebSocketConnection alloc] init]; + + NSString *agent = [conn performSelector:@selector(userAgent) withObject:nil]; + + NSArray *parts = [agent componentsSeparatedByString:@"/"]; + XCTAssertEqual(parts.count, (NSUInteger)5); + XCTAssertEqualObjects(parts[0], @"Firebase"); + XCTAssertEqualObjects(parts[1], kWebsocketProtocolVersion); // Wire protocol version + XCTAssertEqualObjects(parts[2], [FIRDatabase buildVersion]); // Build version + XCTAssertEqualObjects(parts[3], [[UIDevice currentDevice] systemVersion]); // iOS Version +#if TARGET_OS_IPHONE + NSString *deviceName = [UIDevice currentDevice].model; + XCTAssertEqualObjects([parts[4] componentsSeparatedByString:@"_"][0], deviceName); +#endif + +} + +- (void)testKeyComparison { + NSArray *order = @[ + @"-2147483648", @"0", @"1", @"2", @"10", @"2147483647", // Treated as integers + @"-2147483649", @"-2147483650", @"-a", @"2147483648", @"21474836480", @"2147483649", @"a" // treated as strings + ]; + for (NSInteger i = 0; i < order.count; i++) { + for (NSInteger j = i + 1; j < order.count; j++) { + NSString *first = order[i]; + NSString *second = order[j]; + XCTAssertEqual([FUtilities compareKey:first toKey:second], NSOrderedAscending, + @"Expected %@ < %@", first, second); + XCTAssertEqual([FUtilities compareKey:first toKey:first], NSOrderedSame, + @"Expected %@ == %@", first, first); + XCTAssertEqual([FUtilities compareKey:second toKey:first], NSOrderedDescending, + @"Expected %@ > %@", second, first); + } + } +} + +// Enforce a > b, b < a, a != b, because this is apparently something that happens semi-regularly +- (void)testUnicodeKeyComparison { + XCTAssertEqual([FUtilities compareKey:@"유주연" toKey:@"윤규완오빠"], NSOrderedAscending); + XCTAssertEqual([FUtilities compareKey:@"윤규완오빠" toKey:@"유주연"], NSOrderedDescending); + XCTAssertNotEqual([FUtilities compareKey:@"윤규완오빠" toKey:@"유주연"], NSOrderedSame); +} + +@end diff --git a/Example/Database/Tests/en.lproj/InfoPlist.strings b/Example/Database/Tests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/Example/Database/Tests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/Example/Database/Tests/syncPointSpec.json b/Example/Database/Tests/syncPointSpec.json new file mode 100644 index 0000000..f39d29d --- /dev/null +++ b/Example/Database/Tests/syncPointSpec.json @@ -0,0 +1,8203 @@ +[ + { + "name": "Default listen handles a parent set", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Now do a set at the parent. Expect only the 'a' child to get events", + "type": "set", + "path": "", + "data": { + "a": 1, + "b": 2 + }, + "events": [ + { + "path": "a", + "type": "value", + "data": 1 + } + ] + } + ] + }, + + { + "name": "Default listen handles a set at the same level", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Do a set at the same level. Expect the full value to raise events", + "type": "set", + "path": "a", + "data": { + "foo": "bar", + "yes": true + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "a", + "type": "child_added", + "name": "yes", + "prevName": "foo", + "data": true + }, + { + "path": "a", + "type": "value", + "data": { + "foo": "bar", + "yes": true + } + } + ] + } + ] + }, + + { + "name": "A query can get a complete cache then a merge", + "steps": [ + { + "type": "listen", + "path": "a", + "params": { + "tag": 1, + "limitToFirst": 3, + "startAt": {"index": null} + }, + "events": [] + }, + { + "type": "serverUpdate", + "tag": 1, + "path": "a", + "data": { + "a": 1, + "b": 2, + "d": 4 + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": 2 + }, + { + "path": "a", + "type": "child_added", + "name": "d", + "prevName": "b", + "data": 4 + }, + { + "path": "a", + "type": "value", + "data": { + "a": 1, + "b": 2, + "d": 4 + } + } + ] + }, + { + "type": "serverMerge", + "tag": 1, + "path": "a", + "data": { + "a": 5, + "c": 3 + }, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "d", + "data": 4 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": 3 + }, + { + "path": "a", + "type": "child_changed", + "name": "a", + "prevName": null, + "data": 5 + }, + { + "path": "a", + "type": "value", + "data": { + "a": 5, + "b": 2, + "c": 3 + } + } + ] + } + ] + }, + + { + "name": "Server merge on listener with complete children", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 1, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 1 + } + ] + }, + { + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 1 + } + ] + }, + { + "type": "serverMerge", + "path": "a/b", + "data": {"c": 3, "d": 4}, + "events": [ + { + "path": "a/b", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "a/b", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": 4 + }, + { + "path": "a/b", + "type": "value", + "data": {"c": 3, "d": 4} + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": {"c": 3, "d": 4} + } + ] + } + ] + }, + + { + "name": "Empty set doesn't prevent server updates", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "foo": "bar" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "value", + "data": { + "foo" : "bar" + } + } + ] + }, + { + "type": "set", + "path": "empty-path", + "data": null, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { "foo": "new-bar" }, + "events": [ + { + "path": "", + "type": "child_changed", + "name": "foo", + "prevName": null, + "data": "new-bar" + }, + { + "path": "", + "type": "value", + "data": { + "foo" : "new-bar" + } + } + ] + } + ] + }, + + { + "name": "Deep merge on listener with complete children", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + } + ] + }, + { + "type": "listen", + "path": "a/x/y/z", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/x/y/z", + "data": null, + "events": [ + { + "path": "a/x/y/z", + "type": "value", + "data": null + } + ] + }, + { + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + } + ] + }, + { + "type": "serverMerge", + "path": "a/x/y/z", + "data": {"c": 3, "d": 4}, + ".comment": "No events for the top-level listener, since it's not a complete child", + "events": [ + { + "path": "a/x/y/z", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "a/x/y/z", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": 4 + }, + { + "path": "a/x/y/z", + "type": "value", + "data": {"c": 3, "d": 4} + } + ] + } + ] + }, + + { + "name": "Update child listener twice", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 1, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 1 + } + ] + }, + { + "type": "set", + "path": "a/b/c", + "data": "foo", + "events": [ + { + "path": "a/b", + "type": "child_added", + "name": "c", + "prevName": null, + "data": "foo" + }, + { + "path": "a/b", + "type": "value", + "data": {"c": "foo"} + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": {"c": "foo"} + } + ] + } + ] + }, + + { + "name": "Update child of default listen that already has a complete cache", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Fill the listen's cache so we can test a child set with an existing cache", + "type": "serverUpdate", + "path": "a", + "data": { + "b": 2, + "c": 3 + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": 3 + }, + { + "path": "a", + "type": "value", + "data": { + "b": 2, + "c": 3 + } + } + ] + }, + { + ".comment": "Now do a set at a child, expect the child event and a value event", + "type": "set", + "path": "a/b", + "data": 4, + "events": [ + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 4 + }, + { + "path": "a", + "type": "value", + "data": { + "b": 4, + "c": 3 + } + } + ] + } + ] + }, + + { + "name": "Update child of default listen that has no cache", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Now do a set at a child, expect the child event only", + "type": "set", + "path": "a/b", + "data": 4, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 4 + } + ] + } + ] + }, + + { + "name": "Update (via set) the child of a co-located default listener and query", + "steps": [ + { + "type": "listen", + "path": "a", + "params": { + "tag": 1, + "startAt": {"index": null, "name": "b"}, + "endAt": {"index": null, "name": "g"} + }, + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Fill the cache. Since the default listener is there, no tag needed", + "type": "serverUpdate", + "path": "a", + "data": { + "a": 1, + "c": 3, + "d": 4 + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "a", + "data": 3 + }, + { + "path": "a", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": 4 + }, + { + "path": "a", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": 4 + }, + { + "path": "a", + "type": "value", + "data": {"a": 1, "c": 3, "d": 4} + }, + { + "path": "a", + "type": "value", + "data": {"c": 3, "d": 4} + } + ] + }, + { + ".comment": "Cache is primed. Now do the child set", + "type": "set", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": 2 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": {"a": 1, "b":2, "c": 3, "d": 4} + }, + { + "path": "a", + "type": "value", + "data": {"b":2, "c": 3, "d": 4} + } + ] + } + ] + }, + + { + "name": "Update (via set) the child of a query with a full cache", + "steps": [ + { + "type": "listen", + "path": "a", + "params": { + "tag": 1, + "startAt": {"index": null, "name": "b"}, + "endAt": {"index": null, "name": "g"} + }, + "events": [] + }, + { + ".comment": "Fill the cache first", + "type": "serverUpdate", + "path": "a", + "tag": 1, + "data": { + "c": 3, + "d": 4 + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "a", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": 4 + }, + { + "path": "a", + "type": "value", + "data": {"c": 3, "d": 4} + } + ] + }, + { + "type": "set", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": {"b": 2, "c": 3, "d": 4} + } + ] + } + ] + }, + + { + "name": "Update (via set) a child below an empty query", + "steps": [ + { + "type": "listen", + "path": "a", + "params": { + "tag": 1, + "startAt": {"name": "b", "index": null}, + "endAt": {"name": "g", "index": null} + }, + "events": [] + }, + { + ".comment": "Set a single child, outside the window", + "type": "set", + "path": "a/h", + "data": 8, + "events": [] + }, + { + ".comment": "Now set a single child inside the window", + "type": "set", + "path": "a/e", + "data": 5, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "e", + "prevName": null, + "data": 5 + } + ] + } + ] + }, + + { + "name": "Update descendant of default listener with full cache", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Fill the cache", + "type": "serverUpdate", + "path": "a", + "data": { + "b": { + "d": 4 + }, + "e": 5 + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": { + "d": 4 + } + }, + { + "path": "a", + "type": "child_added", + "name": "e", + "prevName": "b", + "data": 5 + }, + { + "path": "a", + "type": "value", + "data": { + "b": { + "d": 4 + }, + "e": 5 + } + } + ] + }, + { + ".comment": "Now do a set at a/b/c, expect child event + new value event", + "type": "set", + "path": "a/b/c", + "data": 3, + "events": [ + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": { + "c": 3, + "d": 4 + } + }, + { + "path": "a", + "type": "value", + "data": { + "b": { + "c": 3, + "d": 4 + }, + "e": 5 + } + } + ] + } + ] + }, + + { + "name": "Descendant set below an empty default listener is ignored", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Now do a set at a/b/c, expect no events", + "type": "set", + "path": "a/b/c", + "data": 3, + "events": [] + } + ] + }, + + { + "name": "Update of a child. This can happen if a child listener is added and removed", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + } + ] + } + ] + }, + + { + "name": "Revert set with only child caches", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + } + ] + }, + { + "type": "set", + "path": "a/b", + "data": 3, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 3 + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 3 + } + ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 2 + } + ] + } + ] + }, + + { + "name": "Can revert a duplicate child set", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + } + ] + }, + { + "type": "set", + "path": "a/b", + "data": 3, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 3 + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 3 + } + ] + }, + { + ".comment": "This set duplicates the data in the previous one, so no events expected", + "type": "set", + "path": "a/b", + "data": 3, + "events": [] + }, + { + ".comment": "Clearing the second set should have no effect, as the underlying set still exists", + "type": "ackUserWrite", + "writeId": 1, + "revert": true, + "events": [] + } + ] + }, + + { + "name": "Can revert a child set and see the underlying data", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + } + ] + }, + { + "type": "set", + "path": "a/b", + "data": 3, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 3 + } + ] + }, + { + "type": "set", + "path": "a/b", + "data": 4, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 4 + } + ] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 3, + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 4 + } + ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "events": [] + }, + { + ".comment": "Clearing the second set should make the underlying set visible again, as it is now confirmed", + "type": "ackUserWrite", + "writeId": 1, + "revert": true, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 3 + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 3 + } + ] + } + ] + }, + + { + "name": "Revert child set with no server data", + "steps": [ + { + "type": "set", + "path": "a/b", + "data": {"d": 4, "e": 5}, + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": {"d": 4, "e": 5} + } + ] + }, + { + "type": "set", + "path": "a/c", + "data": {"z": 26}, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": {"z": 26} + } + ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "b", + "data": {"d": 4, "e": 5} + } + ] + } + ] + }, + + { + "name": "Revert deep set with no server data", + "steps": [ + { + "type": "set", + "path": "a/b/c", + "data": {"d": 4, "e": 5}, + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "set", + "path": "a/x/y", + "data": {"z": 26}, + "events": [] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [] + } + ] + }, + + { + "name": "Revert set covered by non-visible transaction", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + ".comment": "Initial server value is X.", + "type": "serverUpdate", + "path": "", + "data": "X", + "events": [ + { + "path": "", + "type": "value", + "data": "X" + } + ] + }, + { + ".comment": "Set to Y.", + "type": "set", + "path": "", + "data": "Y", + "events": [ + { + "path": "", + "type": "value", + "data": "Y" + } + ] + }, + { + ".comment": "Overwrite with a non-visible 'transaction'.", + "type": "set", + "path": "", + "data": "Z", + "visible": false, + "events": [] + }, + { + ".comment": "Revert set to Y (e.g. security failed), so we should see it go back to Y.", + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [ + { + "path": "", + "type": "value", + "data": "X" + } + ] + } + ] + }, + + { + "name": "Clear parent shadowing server values set with server children", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + } + ] + }, + { + "type": "set", + "path": "a", + "data": {"b": 28, "c": 3}, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 28 + } + ] + }, + { + ".comment": "This listen should get a complete event snap, as well as complete server children", + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 28 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": 3 + }, + { + "path": "a", + "type": "value", + "data": {"b": 28, "c": 3} + } + ] + }, + { + ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though", + "type": "serverUpdate", + "path": "a/b", + "data": 29, + "events": [] + }, + { + ".comment": "Clearing the set should result in updated values for b", + "type": "ackUserWrite", + "writeId": 0, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 29 + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 29 + }, + { + "path": "a", + "type": "value", + "data": {"b": 29, "c": 3} + } + ] + } + ] + }, + + { + "name": "Clear child shadowing server values set with server children", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + } + ] + }, + { + "type": "set", + "path": "a/b", + "data": 28, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 28 + } + ] + }, + { + ".comment": "This listen should get an event child snap, as well as a complete server child: b", + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 28 + } + ] + }, + { + ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though", + "type": "serverUpdate", + "path": "a/b", + "data": 29, + "events": [] + }, + { + ".comment": "Clearing the set should result in no events. We don't yet have the server data at the parent", + "type": "ackUserWrite", + "writeId": 0, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 29 + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 29 + } + ] + } + ] + }, + + { + "name": "Unrelated merge doesn't shadow server updates", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": null, + "events": [ + { + "path": "a", + "type": "value", + "data": null + } + ] + }, + { + "type": "update", + "path": "a", + "data": {"b": 2, "c": 3}, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": 3 + }, + { + "path": "a", + "type": "value", + "data": {"b": 2, "c": 3} + } + ] + }, + { + "type": "serverUpdate", + "path": "a/d", + "data": 4, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": 4 + }, + { + "path": "a", + "type": "value", + "data": {"b": 2, "c": 3, "d": 4} + } + ] + } + ] + }, + + { + "name": "Can set alongside a remote merge", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": null, + "events": [ + { + "path": "a", + "type": "value", + "data": null + } + ] + }, + { + "type": "set", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": {"b": 2} + } + ] + }, + { + "type": "serverMerge", + "path": "a", + "data": {"b": 28, "c": 3}, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": 3 + }, + { + "path": "a", + "type": "value", + "data": {"b": 2, "c": 3} + } + ] + } + ] + }, + + { + "name": "setPriority on a location with no cache", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "set", + "path": "a/.priority", + "data": "foo", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": "bar", + "events": [ + { + "path": "a", + "type": "value", + "data": { ".priority": "foo", ".value": "bar" } + } + ] + } + ] + }, + + { + "name": "deep update deletes child from limit window and pulls in new child", + "steps": [ + { + "type": "set", + "path": "a", + "data": { + "a": {"aa": 2, "aaa": 3}, + "b": {"bb": 2, "bbb": 3}, + "c": {"cc": 2, "ccc": 3} + }, + "events": [] + }, + { + "type": "listen", + "path": "a", + "params": { + "tag": 1, + "limitToLast": 2 + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": {"bb": 2, "bbb": 3} + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": {"cc": 2, "ccc": 3} + }, + { + "path": "a", + "type": "value", + "data": { + "b": {"bb": 2, "bbb": 3}, + "c": {"cc": 2, "ccc": 3} + } + } + ] + }, + { + "type": "update", + "path": "a/b", + "data": { + "bb": null, + "bbb": null + }, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "b", + "data": {"bb": 2, "bbb": 3} + }, + { + "path": "a", + "type": "child_added", + "name": "a", + "prevName": null, + "data": {"aa": 2, "aaa": 3} + }, + { + "path": "a", + "type": "value", + "data": { + "a": {"aa": 2, "aaa": 3}, + "c": {"cc": 2, "ccc": 3} + } + } + ] + } + ] + }, + + { + "name": "deep set deletes child from limit window and pulls in new child", + "steps": [ + { + "type": "set", + "path": "a", + "data": { + "a": {"aa": 2}, + "b": {"bb": 2}, + "c": {"cc": 2} + }, + "events": [] + }, + { + "type": "listen", + "path": "a", + "params": { + "tag": 1, + "limitToLast": 2 + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": {"bb": 2} + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": {"cc": 2} + }, + { + "path": "a", + "type": "value", + "data": { + "b": {"bb": 2}, + "c": {"cc": 2} + } + } + ] + }, + { + "type": "set", + "path": "a/b/bb", + "data": null, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "b", + "data": {"bb": 2} + }, + { + "path": "a", + "type": "child_added", + "name": "a", + "prevName": null, + "data": {"aa": 2} + }, + { + "path": "a", + "type": "value", + "data": { + "a": {"aa": 2}, + "c": {"cc": 2} + } + } + ] + } + ] + }, + + { + "name": "Edge case in newChildForChange_", + "steps": [ + { + "type": "listen", + "path": "a/d", + "events": [] + }, + { + "type": "listen", + "path": "a/b/c", + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/d", + "data": 4, + "events": [ + { + "type": "value", + "path": "a/d", + "data": 4 + }, + { + "type": "child_added", + "path": "a", + "name": "d", + "prevName": null, + "data": 4 + } + ] + }, + { + "type": "serverUpdate", + "path": "a/b/c", + "data": 3, + "events": [ + { + "path": "a/b/c", + "type": "value", + "data": 3 + } + ] + } + ] + }, + + { + "name": "Revert set in query window", + "steps": [ + { + "type": "listen", + "path": "a", + "params": { + "limitToLast": 1, + "tag": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "tag": 1, + "data": {"b": 2}, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": {"b": 2} + } + ] + }, + { + "type": "set", + "path": "a/c", + "data": 3, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "b", + "data": 2 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "a", + "type": "value", + "data": {"c": 3} + } + ] + }, + { + "type": "ackUserWrite", + "revert": true, + "writeId": 0, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "c", + "data": 3 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": {"b": 2} + } + ] + } + ] + }, + + { + "name": "Handles a server value moving a child out of a query window", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}}, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": {"c": {"value": 3}, "d": {"value": 4}} + }, + { + "path": "a", + "type": "value", + "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}} + } + ] + }, + { + "type": "listen", + "params": { + "tag": 1, + "limitToLast": 1, + "orderBy": "value" + }, + "path": "a/b", + "events": [ + { + "path": "a/b", + "type": "child_added", + "name": "d", + "prevName": null, + "data": {"value": 4} + }, + { + "path": "a/b", + "type": "value", + "data": {"d": {"value": 4}} + } + ] + }, + { + "type": "set", + "path": "a/b/d/value", + "data": 5, + "events": [ + { + "path": "a/b", + "type": "child_moved", + "name": "d", + "prevName": null, + "data": {"value": 5} + }, + { + "path": "a/b", + "type": "child_changed", + "name": "d", + "prevName": null, + "data": {"value": 5} + }, + { + "path": "a/b", + "type": "value", + "data": {"d": {"value": 5}} + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": {"c": {"value": 3}, "d": {"value": 5}} + }, + { + "path": "a", + "type": "value", + "data": {"b": {"c": {"value": 3}, "d": {"value": 5}}} + } + ] + }, + { + ".comment": "The query is shadowed, so only one data update arrives. We're simulating a server value, so it's different than what was set", + "type": "serverUpdate", + "path": "a/b/d/value", + "data": 2, + "events": [] + }, + { + ".comment": "Now that we're acking the write, we should see the effect of the change", + "type": "ackUserWrite", + "writeId": 0, + "events": [ + { + "path": "a/b", + "type": "child_removed", + "name": "d", + "data": {"value": 5} + }, + { + "path": "a/b", + "type": "child_added", + "name": "c", + "prevName": null, + "data": {"value": 3} + }, + { + "path": "a/b", + "type": "value", + "data": {"c": {"value": 3}} + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": {"c": {"value": 3}, "d": {"value": 2}} + }, + { + "path": "a", + "type": "value", + "data": {"b": {"c": {"value": 3}, "d": {"value": 2}}} + } + ] + } + ] + }, + + { + "name": "Update of indexed child works", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}}, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": {"c": {"value": 3}, "d": {"value": 4}} + }, + { + "path": "a", + "type": "value", + "data": {"b": {"c": {"value": 3}, "d": {"value": 4}}} + } + ] + }, + { + "type": "listen", + "params": { + "tag": 1, + "limitToLast": 1, + "orderBy": "value" + }, + "path": "a/b", + "events": [ + { + "path": "a/b", + "type": "child_added", + "name": "d", + "prevName": null, + "data": {"value": 4} + }, + { + "path": "a/b", + "type": "value", + "data": {"d": {"value": 4}} + } + ] + }, + { + "type": "update", + "path": "a/b/c", + "data": {"value": 5}, + "events": [ + { + "path": "a/b", + "type": "child_removed", + "name": "d", + "data": {"value": 4} + }, + { + "path": "a/b", + "type": "child_added", + "name": "c", + "prevName": null, + "data": {"value": 5} + }, + { + "path": "a/b", + "type": "value", + "data": {"c": {"value": 5}} + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": {"c": {"value": 5}, "d": {"value": 4}} + }, + { + "path": "a", + "type": "value", + "data": {"b": {"c": {"value": 5}, "d": {"value": 4}}} + } + ] + } + ] + }, + + { + "name": "Merge applied to empty limit", + "steps": [ + { + "type": "listen", + "path": "a", + "params": { + "limitToLast": 1, + "tag": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "tag": 1, + "data": null, + "events": [ + { + "path": "a", + "type": "value", + "data": null + } + ] + }, + { + "type": "update", + "path": "a", + "data": {"b": 1}, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 1 + }, + { + "path": "a", + "type": "value", + "data": {"b": 1} + } + ] + } + ] + }, + + { + "name": "Limit is refilled from server data after merge", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "listen", + "path": "a/b", + "params": { + "tag": 1, + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": {"a": 1, "b": {"c": 3, "d": 4}}, + "events": [ + { + "path": "a/b", + "type": "child_added", + "name": "d", + "prevName": null, + "data": 4 + }, + { + "path": "a/b", + "type": "value", + "data": {"d": 4} + }, + { + "path": "a", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": {"c": 3, "d": 4} + }, + { + "path": "a", + "type": "value", + "data": {"a": 1, "b": {"c": 3, "d": 4}} + } + ] + }, + { + "type": "update", + "path": "a/b", + "data": {"d": null}, + "events": [ + { + "path": "a/b", + "type": "child_removed", + "name": "d", + "data": 4 + }, + { + "path": "a/b", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "a/b", + "type": "value", + "data": {"c": 3} + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": "a", + "data": {"c": 3} + }, + { + "path": "a", + "type": "value", + "data": {"a": 1, "b": {"c": 3}} + } + ] + } + ] + }, + + { + "name": "Handle repeated listen with merge as first update", + "steps": [ + { + ".comment": "Assume that we just unlistened on this path, and before the unlisten arrives, a merge was sent by the server", + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "This happens when a merge arriving from the server while the 2nd listen is in flight", + "type": "serverMerge", + "path": "a", + "data": {"c": 3}, + "events": [] + } + ] + }, + + { + "name": "Limit is refilled from server data after set", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "listen", + "path": "a/b", + "params": { + "tag": 1, + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": {"a": 1, "b": {"c": 3, "d": 4}}, + "events": [ + { + "path": "a/b", + "type": "child_added", + "name": "d", + "prevName": null, + "data": 4 + }, + { + "path": "a/b", + "type": "value", + "data": {"d": 4} + }, + { + "path": "a", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": {"c": 3, "d": 4} + }, + { + "path": "a", + "type": "value", + "data": {"a": 1, "b": {"c": 3, "d": 4}} + } + ] + }, + { + "type": "set", + "path": "a/b/d", + "data": null, + "events": [ + { + "path": "a/b", + "type": "child_removed", + "name": "d", + "data": 4 + }, + { + "path": "a/b", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "a/b", + "type": "value", + "data": {"c": 3} + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": "a", + "data": {"c": 3} + }, + { + "path": "a", + "type": "value", + "data": {"a": 1, "b": {"c": 3}} + } + ] + } + ] + }, + + { + "name": "query on weird path.", + ".comment": "We used to use '|' as a separator, which broke with paths containing |", + "steps": [ + { + "type": "listen", + "path": "foo|!@%^&*()_<>?+={}blah", + "params": { + "tag": 1, + "limitToLast": 5 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo|!@%^&*()_<>?+={}blah", + "tag": 1, + "data": { "a": "a" }, + "events": [ + { + "path": "foo|!@%^&*()_<>?+={}blah", + "type": "child_added", + "name": "a", + "prevName": null, + "data": "a" + }, + { + "path": "foo|!@%^&*()_<>?+={}blah", + "type": "value", + "data": { "a": "a" } + } + ] + } + ] + }, + + { + "name": "runs, round2", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "set", + "path": "foo", + "data": "baz", + "events": [ + { + "path": "foo", + "type": "value", + "data": "baz" + } + ] + }, + { + "type": "set", + "path": "foo/new", + "data": "bar", + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "new", + "prevName": null, + "data": "bar" + }, + { + "path": "foo", + "type": "value", + "data": { "new" : "bar"} + } + ] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": "baz", + "events": [] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": { "new" : "bar"}, + "events": [] + }, + { + "type": "ackUserWrite", + "writeId": 1, + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": { "new" : true, "other" : "bar"}, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "other", + "prevName": "new", + "data": "bar" + }, + { + "path": "foo", + "type": "child_changed", + "name": "new", + "prevName": null, + "data": true + }, + { + "path": "foo", + "type": "value", + "data": { "new": true, "other": "bar"} + } + ] + } + ] + }, + + { + "name": "handles nested listens", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "listen", + "path": "foo/bar", + "events": [] + }, + { + "type": "set", + "path": "", + "data": { + "foo": { + "a": 1, + "b": 2, + "bar": { + "c": true, + "d": false + } + }, + "baz": false + }, + "events": [ + { + "path": "foo/bar", + "type": "child_added", + "name": "c", + "prevName": null, + "data": true + }, + { + "path": "foo/bar", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": false + }, + { + "path": "foo/bar", + "type": "value", + "data": {"c": true, "d": false} + }, + { + "path": "foo", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + }, + { + "path": "foo", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": 2 + }, + { + "path": "foo", + "type": "child_added", + "name": "bar", + "prevName": "b", + "data": {"c": true, "d": false} + }, + { + "path": "foo", + "type": "value", + "data": { + "a": 1, + "b": 2, + "bar": { + "c": true, + "d": false + } + } + } + ] + }, + { + "type": "set", + "path": "", + "data": { + "foo": { + "a": 1, + "b": 2, + "bar": { + "c": false, + "d": false, + "e": true + }, + "f": 3 + }, + "baz": false + }, + "events": [ + { + "path": "foo/bar", + "type": "child_added", + "name": "e", + "prevName": "d", + "data": true + }, + { + "path": "foo/bar", + "type": "child_changed", + "name": "c", + "prevName": null, + "data": false + }, + { + "path": "foo/bar", + "type": "value", + "data": { + "c": false, + "d": false, + "e": true + } + }, + { + "path": "foo", + "type": "child_added", + "name": "f", + "prevName": "bar", + "data": 3 + }, + { + "path": "foo", + "type": "child_changed", + "name": "bar", + "prevName": "b", + "data": { + "c": false, + "d": false, + "e": true + } + }, + { + "path": "foo", + "type": "value", + "data": { + "a": 1, + "b": 2, + "bar": { + "c": false, + "d": false, + "e": true + }, + "f": 3 + } + } + ] + }, + { + ".comment": "Duplicate set, no events raised", + "type": "set", + "path": "", + "data": { + "foo": { + "a": 1, + "b": 2, + "bar": { + "c": false, + "d": false, + "e": true + }, + "f": 3 + }, + "baz": false + }, + "events": [] + } + ] + }, + + { + "name": "Handles a set below a listen", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "set", + "path": "foo", + "data": 1, + ".comment": "We only expect a child_added, since it does not completely fill the view", + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": 1 + } + ] + } + ] + }, + + { + "name": "does non-default queries", + "steps": [ + { + "type": "listen", + "path": "foo", + "params": { + "tag": 1, + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "set", + "path": "", + "data": { + "foo": { + "a": 1, + "b": 2 + } + }, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "foo", + "type": "value", + "data": { + "b": 2 + } + } + ] + }, + { + ".comment": "Now have the server send the same data to the query. No events result because there is no change", + "type": "serverUpdate", + "tag": 1, + "path": "foo", + "data": { + "b": 2 + }, + "events": [] + } + ] + }, + + { + "name": "handles a co-located default listener and query", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "listen", + "path": "foo", + "params": { + "tag": 1, + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "set", + "path": "foo", + "data": { "a": 1, "b": 2}, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + }, + { + "path": "foo", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": 2 + }, + { + "path": "foo", + "type": "value", + "data": { "a": 1, "b": 2} + }, + { + "path": "foo", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 2 + }, + { + "path": "foo", + "type": "value", + "data": {"b": 2} + } + ] + } + ] + }, + + { + "name": "Default and non-default listener at same location with server update", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "listen", + "path": "foo", + "params": { + "tag": 1, + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": {"a": 1, "b": 2}, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "a", + "data": 1, + "prevName": null + }, + { + "path": "foo", + "type": "child_added", + "name": "b", + "data": 2, + "prevName": "a" + }, + { + "path": "foo", + "type": "value", + "data": {"a": 1, "b": 2} + }, + { + "path": "foo", + "type": "child_added", + "name": "b", + "data": 2, + "prevName": null + }, + { + "path": "foo", + "type": "value", + "data": {"b": 2} + } + ] + } + ] + }, + + { + "name": "Add a parent listener to a complete child listener, expect child event", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": 1, + "events": [ + { + "path": "foo", + "type": "value", + "data": 1 + } + ] + }, + { + "type": "listen", + "path": "", + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": 1 + } + ] + } + ] + }, + + { + "name": "Add listens to a set, expect correct events, including a child event", + "steps": [ + { + "type": "set", + "path": "foo", + "data": {"bar": 1, "baz": 2}, + "events": [] + }, + { + "type": "listen", + "path": "foo/bar", + "events": [ + { + "path": "foo/bar", + "type": "value", + "data": 1 + } + ] + }, + { + "type": "listen", + "path": "", + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": {"bar": 1, "baz": 2} + } + ] + } + ] + }, + + { + "name": "ServerUpdate to a child listener raises child events at parent", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": 1, + "events": [ + { + "path": "foo", + "type": "value", + "data": 1 + }, + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": 1 + } + ] + } + ] + }, + + { + "name": "ServerUpdate to a child listener raises child events at parent query", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": 1, + "events": [ + { + "path": "foo", + "type": "value", + "data": 1 + }, + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": 1 + } + ] + } + ] + }, + + { + "name": "Multiple complete children are handled properly", + "steps": [ + { + "type": "listen", + "path": "foo/a", + "events": [] + }, + { + "type": "listen", + "path": "foo/b", + "events": [] + }, + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo/a", + "data": 1, + "events": [ + { + "path": "foo/a", + "type": "value", + "data": 1 + }, + { + "path": "foo", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + } + ] + }, + { + "type": "serverUpdate", + "path": "foo/b", + "data": 2, + "events": [ + { + "path": "foo/b", + "type": "value", + "data": 2 + }, + { + "path": "foo", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": 2 + } + ] + } + ] + }, + + { + "name": "Write leaf node, overwrite at parent node", + "steps": [ + { + "type": "listen", + "path": "a/aa", + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "set", + "path": "a/aa", + "data": 1, + "events": [ + { + "path": "a/aa", + "type": "value", + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "aa", + "prevName": null, + "data": 1 + } + ] + }, + { + "type": "set", + "path": "a", + "data": { + "aa": 2 + }, + "events": [ + { + "path": "a/aa", + "type": "value", + "data": 2 + }, + { + "path": "a", + "type": "child_changed", + "name": "aa", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": { + "aa": 2 + } + } + ] + } + ] + }, + + { + "name": "Confirm complete children from the server", + "steps": [ + { + "type": "listen", + "path": "a/aa", + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/aa", + "data": 1, + "events": [ + { + "path": "a/aa", + "type": "value", + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "aa", + "prevName": null, + "data": 1 + } + ] + }, + { + "type": "serverUpdate", + "path": "a", + ".comment": "At some point in the future, we might consider sending a hash here to avoid duplicate data", + "data": {"aa": 1}, + "events": [ + { + "path": "a", + "type": "value", + "data": {"aa": 1} + } + ] + }, + { + ".comment": "Now, delete the same child and make sure we get the right events", + "type": "serverUpdate", + "path": "a/aa", + "data": null, + "events": [ + { + "path": "a/aa", + "type": "value", + "data": null + }, + { + "path": "a", + "type": "child_removed", + "name": "aa", + "prevName": null, + "data": 1 + }, + { + "path": "a", + "type": "value", + "data": null + } + ] + } + ] + }, + + { + "name": "Write leaf, overwrite from parent", + "steps": [ + { + "type": "listen", + "path": "a/aa", + "events": [] + }, + { + "type": "listen", + "path": "a/bb", + "events": [] + }, + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "First set is at leaf. Expect only a child_added for the parent, nothing for the sibling", + "type": "set", + "path": "a/aa", + "data": 1, + "events": [ + { + "path": "a/aa", + "type": "value", + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "name": "aa", + "prevName": null, + "data": 1 + } + ] + }, + { + ".comment": "Now set at the parent. Expect value events for everyone", + "type": "set", + "path": "a", + "data": {"aa": 2}, + "events": [ + { + "path": "a/aa", + "type": "value", + "data": 2 + }, + { + "path": "a/bb", + "type": "value", + "data": null + }, + { + "path": "a", + "type": "child_changed", + "name": "aa", + "prevName": null, + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": {"aa": 2} + } + ] + } + ] + }, + + { + "name": "Basic update test", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "listen", + "path": "b", + "events": [] + }, + { + "type": "listen", + "path": "", + "events": [] + }, + { + ".comment": "Initial data", + "type": "serverUpdate", + "path": "", + "data": { + "a": 1, + "b": 2, + "c": 3 + }, + "events": [ + { + "path": "a", + "type": "value", + "data": 1 + }, + { + "path": "b", + "type": "value", + "data": 2 + }, + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": 1 + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": 2 + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": 3 + }, + { + "path": "", + "type": "value", + "data": { + "a": 1, + "b": 2, + "c": 3 + } + } + ] + }, + { + ".comment": "Now update two children. Not b, there should be no events at b", + "type": "update", + "path": "", + "data": { + "a": true, + "c": false + }, + "events": [ + { + "path": "a", + "type": "value", + "data": true + }, + { + "path": "", + "type": "child_changed", + "name": "a", + "prevName": null, + "data": true + }, + { + "path": "", + "type": "child_changed", + "name": "c", + "prevName": "b", + "data": false + }, + { + "path": "", + "type": "value", + "data": { + "a": true, + "b": 2, + "c": false + } + } + ] + } + ] + }, + { + "name": "No double value events for user ack", + "steps": [ + { + "type": "listen", + "path": "foo", + "params": { + "tag": 1, + "limitToLast": 1, + "endAt": {"index": null, "name": "d"} + }, + "events": [] + }, + { + ".comment": "user sets data", + "type": "set", + "path": "foo", + "data": { + "a": 1, + "b": 2, + "c": 3 + }, + "events": [ + { + "path": "foo", + "type": "value", + "data": { "c": 3 } + }, + { + "path": "foo", + "type": "child_added", + "name": "c", + "prevName": null, + "data": 3 + } + ] + }, + { + ".comment": "server acks data, but local overwrite causes no events to fire", + "type": "serverUpdate", + "path": "foo", + "tag": 1, + "data": null, + "events": [] + }, + { + ".comment": "server sends data with merge", + "type": "serverMerge", + "path": "foo", + "tag": 1, + "data": { + "c": 3 + }, + "events": [ ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": false, + "events": [] + }, + { + "type": "set", + "path": "foo/d", + "data": 4, + "events": [ + { + "path": "foo", + "type": "value", + "data": { "d": 4 } + }, + { + "path": "foo", + "type": "child_removed", + "name": "c", + "prevName": null, + "data": 3 + }, + { + "path": "foo", + "type": "child_added", + "name": "d", + "prevName": null, + "data": 4 + } + ] + }, + { + "type": "serverMerge", + "path": "foo", + "tag": 1, + "data": { + "d": 4 + }, + "events": [ ] + }, + { + "type": "ackUserWrite", + "writeId": 1, + "revert": false, + "events": [] + } + ] + }, + { + "name": "Basic key index sanity check", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderByKey": true, + "startAt": { "index": "aa" }, + "endAt": { "index": "e" } + }, + "events": [] + }, + { + "type": "set", + "path": "", + "data": { + "a": { ".priority": 10, ".value": "a" }, + "b": { ".priority": 5, ".value": "b" }, + "c": { ".priority": 20, ".value": "c" }, + "d": { ".priority": 7, ".value": "d" }, + "e": { ".priority": 30, ".value": "e" }, + "f": { ".priority": 8, ".value": "f" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": null, + "data": {".priority": 5, ".value": "b"} + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": {".priority": 20, ".value": "c"} + }, + { + "path": "", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": {".priority": 7, ".value": "d"} + }, + { + "path": "", + "type": "child_added", + "name": "e", + "prevName": "d", + "data": {".priority": 30, ".value": "e"} + }, + { + "path": "", + "type": "value", + "data": { + "b": { ".priority": 5, ".value": "b" }, + "c": { ".priority": 20, ".value": "c" }, + "d": { ".priority": 7, ".value": "d" }, + "e": { ".priority": 30, ".value": "e" } + } + } + ] + }, + { + ".comment": "Add a new item outside of range and make sure we get events.", + "type": "set", + "path": "a", + "data": "hello!", + "events": [ ] + }, + { + ".comment": "Add a new item within range and ensure we get ld_added.", + "type": "set", + "path": "bass", + "data": 3.14, + "events": + [ + { + "path": "", + "type": "child_added", + "name": "bass", + "prevName": "b", + "data": 3.14 + }, + { + "path": "", + "type": "value", + "data": { + "b": { ".priority": 5, ".value": "b" }, + "bass": 3.14, + "c": { ".priority": 20, ".value": "c" }, + "d": { ".priority": 7, ".value": "d" }, + "e": { ".priority": 30, ".value": "e" } + } + } + ] + }, + { + ".comment": "Modify an item and ensure we get child_changed.", + "type": "set", + "path": "b", + "data": 42, + "events": + [ + { + "path": "", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 42 + }, + { + "path": "", + "type": "value", + "data": { + "b": 42, + "bass": 3.14, + "c": { ".priority": 20, ".value": "c" }, + "d": { ".priority": 7, ".value": "d" }, + "e": { ".priority": 30, ".value": "e" } + } + } + ] + } + ] + }, + { + "name": "Collect correct subviews to listen on", + "steps": + [ + { + "type": "listen", + "callbackId": 1, + "path": "", + "events": [] + }, + { + "type": "listen", + "callbackId": 1, + "path": "/a", + "events": [] + }, + { + "type": "listen", + "callbackId": 1, + "path": "/a/b", + "events": [] + }, + { + ".comment": "should not cause /a/b to be listened upon", + "type": "unlisten", + "callbackId": 1, + "path": "", + "events": [] + }, + { + ".comment": "should now cause /a/b to be listened upon", + "type": "unlisten", + "callbackId": 1, + "path": "/a", + "events": [] + } + ] + }, + { + "name": "Limit to first one on ordered query", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "vanished", + "limitToFirst": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "triceratops": {"vanished": -66000000}, + "stegosaurus": {"vanished": -155000000}, + "pterodactyl": {"vanished": -75000000} + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "stegosaurus", + "prevName": null, + "data": {"vanished": -155000000} + }, + { + "path": "", + "type": "value", + "name": "", + "prevName": null, + "data": {"stegosaurus": {"vanished": -155000000}} + } + ] + } + ] + }, + { + "name": "Limit to last one on ordered query", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "vanished", + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "triceratops": {"vanished": -66000000}, + "stegosaurus": {"vanished": -155000000}, + "pterodactyl": {"vanished": -75000000} + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "triceratops", + "prevName": null, + "data": {"vanished": -66000000} + }, + { + "path": "", + "type": "value", + "name": "", + "prevName": null, + "data": {"triceratops": {"vanished": -66000000}} + } + ] + } + ] + }, + { + "name": "Update indexed value on existing child from limited query", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "age", + "limitToLast": 4 + }, + "events": [] + }, + { + "type": "serverUpdate", + "tag": 1, + "path": "", + "data": { + "4": { "age": 41, "highscore": 400, "name": "old mama"}, + "5": { "age": 18, "highscore": 1200, "name": "young mama"}, + "6": { "age": 20, "highscore": 1003, "name": "micheal blub"}, + "7": { "age": 30, "highscore": 10000, "name": "no. 7"} + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "4", + "prevName": "7", + "data": { "age": 41, "highscore": 400, "name": "old mama"} + }, + { + "path": "", + "type": "child_added", + "name": "5", + "prevName": null, + "data": { "age": 18, "highscore": 1200, "name": "young mama"} + }, + { + "path": "", + "type": "child_added", + "name": "6", + "prevName": "5", + "data": { "age": 20, "highscore": 1003, "name": "micheal blub"} + }, + { + "path": "", + "type": "child_added", + "name": "7", + "prevName": "6", + "data": { "age": 30, "highscore": 10000, "name": "no. 7"} + }, + { + "path": "", + "type": "value", + "data": { + "4": { "age": 41, "highscore": 400, "name": "old mama"}, + "5": { "age": 18, "highscore": 1200, "name": "young mama"}, + "6": { "age": 20, "highscore": 1003, "name": "micheal blub"}, + "7": { "age": 30, "highscore": 10000, "name": "no. 7"} + } + } + ] + }, + { + ".comment": "update the order by value, should cause a new value event", + "type": "serverUpdate", + "path": "4/age", + "tag": 1, + "data": 25, + "events": [ + { + "path": "", + "type": "child_moved", + "name": "4", + "prevName": "6", + "data": { "age": 25, "highscore": 400, "name": "old mama"} + }, + { + "path": "", + "type": "child_changed", + "name": "4", + "prevName": "6", + "data": { "age": 25, "highscore": 400, "name": "old mama"} + }, + { + "path": "", + "type": "value", + "data": { + "4": { "age": 25, "highscore": 400, "name": "old mama"}, + "5": { "age": 18, "highscore": 1200, "name": "young mama"}, + "6": { "age": 20, "highscore": 1003, "name": "micheal blub"}, + "7": { "age": 30, "highscore": 10000, "name": "no. 7"} + } + } + ] + } + ] + }, + + { + "name": "Can create startAt, endAt, equalTo queries with bool", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "boolKey", + "startAt": {"index": true} + }, + "events": [] + }, + { + "type": "listen", + "path": "", + "params": { + "tag": 2, + "orderBy": "boolKey", + "endAt": {"index": true} + }, + "events": [] + }, + { + "type": "listen", + "path": "", + "params": { + "tag": 3, + "orderBy": "boolKey", + "equalTo": {"index": true} + }, + "events": [] + }, + { + "type": "suppressWarning", + "events": [] + } + ] + }, + { + "name": "Query with existing server snap", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "age" + }, + "events": [] + }, + { + ".comment": "untagged update, since index doesn't exist", + "type": "serverUpdate", + "path": "", + "data": { + "foo": { "age": 10, "score": 100, "bar": "baz" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": { "age": 10, "score": 100, "bar": "baz" } + }, + { + "path": "", + "type": "value", + "data": { + "foo": { "age": 10, "score": 100, "bar": "baz" } + } + } + ] + }, + { + ".comment": "new listen should use existing data and index correctly", + "type": "listen", + "path": "", + "params": { + "tag": 2, + "orderBy": "score" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": { "age": 10, "score": 100, "bar": "baz" } + }, + { + "path": "", + "type": "value", + "data": { + "foo": { "age": 10, "score": 100, "bar": "baz" } + } + } + ] + } + ] + }, + { + "name": "Server data is not purged for non-server-indexed queries", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "highscore", + "limitToLast": 2 + }, + "events": [] + }, + { + ".comment": "server has no index, so it sends down everything", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "highscore": 100, "value": "a" }, + "b": { "highscore": 200, "value": "b" }, + "c": { "highscore": 0, "value": "c" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "highscore": 100, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "highscore": 200, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "highscore": 100, "value": "a" }, + "b": { "highscore": 200, "value": "b" } + } + } + ] + }, + { + ".comment": "update of highscore leads to only a partial update", + "type": "serverUpdate", + "path": "c/highscore", + "data": 300, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "prevName": null, + "data": { "highscore": 100, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "highscore": 300, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "b": { "highscore": 200, "value": "b" }, + "c": { "highscore": 300, "value": "c" } + } + } + ] + } + ] + }, + { + "name": "Limit with custom orderBy is refilled with correct item", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "age", + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "a": { "age": 4 }, + "b": { "age": 3 }, + "c": { "age": 2 }, + "d": { "age": 1 } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "age": 4 } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "age": 4 } + } + } + ] + }, + { + ".comment": "delete 'a' and make sure 'b' comes into view.", + "type": "set", + "path": "a", + "data": null, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { "age": 4 } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": null, + "data": { "age": 3 } + }, + { + "path": "", + "type": "value", + "data": { + "b": { "age": 3 } + } + } + ] + } + ] + }, + { + "name": "startAt/endAt dominates limit", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": 1 }, + "endAt": { "index": 2 }, + "limitToFirst": 2 + }, + "events": [] + }, + { + ".comment": "server has no index, so it sends down everything", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 1000, "value": "b" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" } + } + } + ] + }, + { + ".comment": "update from server to fill limit and beyond", + "type": "serverMerge", + "path": "", + "data": { + "b": { "index": 1, "value": "b" }, + "c": { "index": 2, "value": "c" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 1, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 1, "value": "b" } + } + } + ] + }, + { + ".comment": "update from server to move entry out of window", + "type": "serverUpdate", + "path": "a/index", + "data": 1000, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "index": 2, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "b": { "index": 1, "value": "b" }, + "c": { "index": 2, "value": "c" } + } + } + ] + }, + { + ".comment": "update from server to move all but one entry out of window", + "type": "serverUpdate", + "path": "b/index", + "data": 1000, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "b", + "data": { "index": 1, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "c": { "index": 2, "value": "c" } + } + } + ] + } + ] + }, + { + "name": "Update to single child that moves out of window", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": 1 }, + "endAt": { "index": 10 }, + "limitToFirst": 2 + }, + "events": [] + }, + { + ".comment": "update from server sends all data", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 2, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" } + } + } + ] + }, + { + ".comment": "update from server to move child out of query", + "type": "serverUpdate", + "path": "a/index", + "data": -1, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "index": 3, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" } + } + } + ] + }, + { + ".comment": "update from server to move child out of query", + "type": "serverUpdate", + "path": "b/index", + "data": -1, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "b", + "data": { "index": 2, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "c": { "index": 3, "value": "c" } + } + } + ] + } + ] + }, + { + "name": "Limited query doesn't pull in out of range child", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": 1 }, + "endAt": { "index": 10 }, + "limitToFirst": 2 + }, + "events": [] + }, + { + ".comment": "update from server sends all data", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 1000, "value": "c" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 2, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" } + } + } + ] + }, + { + ".comment": "update from server to move child out of query", + "type": "serverUpdate", + "path": "a/index", + "data": -1, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "value", + "data": { + "b": { "index": 2, "value": "b" } + } + } + ] + } + ] + }, + { + "name": "Merge for location with default and limited listener", + "steps": + [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + ".comment": "complete update", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" }, + "d": { "index": 4, "value": "d" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 2, "value": "b" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "index": 3, "value": "c" } + }, + { + "path": "", + "type": "child_added", + "name": "d", + "prevName": "c", + "data": { "index": 4, "value": "d" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" }, + "d": { "index": 4, "value": "d" } + } + } + ] + }, + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "limitToFirst": 2 + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 2, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" } + } + } + ] + }, + { + ".comment": "update from server pulls in other node", + "type": "serverMerge", + "path": "", + "data": { + "a": null, + "d": null + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_removed", + "name": "d", + "data": { "index": 4, "value": "d" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "index": 3, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" } + } + }, + { + "path": "", + "type": "value", + "data": { + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" } + } + } + ] + } + ] + }, + { + "name": "User merge pulls in correct values", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": 1 }, + "endAt": { "index": 10 }, + "limitToFirst": 3 + }, + "events": [] + }, + { + ".comment": "update from server sends all data", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" }, + "d": { "index": 1000, "value": "d" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 2, "value": "b" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "index": 3, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" } + } + } + ] + }, + { + ".comment": "user merge pulls in existing value", + "type": "update", + "path": "d", + "data": { "index": 2 }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "c", + "data": { "index": 3, "value": "c" } + }, + { + "path": "", + "type": "child_added", + "name": "d", + "prevName": "b", + "data": { "index": 2, "value": "d" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "d": { "index": 2, "value": "d" } + } + } + ] + } + ] + }, + { + "name": "User deep set pulls in correct values", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": 1 }, + "endAt": { "index": 10 }, + "limitToFirst": 3 + }, + "events": [] + }, + { + ".comment": "update from server sends all data", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" }, + "d": { "index": 1000, "value": "d" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 2, "value": "b" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "index": 3, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "c": { "index": 3, "value": "c" } + } + } + ] + }, + { + ".comment": "user deep set pulls in existing value", + "type": "set", + "path": "d/index", + "data": 2, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "c", + "data": { "index": 3, "value": "c" } + }, + { + "path": "", + "type": "child_added", + "name": "d", + "prevName": "b", + "data": { "index": 2, "value": "d" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 2, "value": "b" }, + "d": { "index": 2, "value": "d" } + } + } + ] + } + ] + }, + { + "name": "Queries with equalTo(null) work", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": null }, + "endAt": { "index": null } + }, + "events": [] + }, + { + ".comment": "update from server sends all data", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "value": "a" }, + "b": { "value": "b" }, + "c": { "value": "c", "index": 1 } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "value": "a" }, + "b": { "value": "b" } + } + } + ] + }, + { + ".comment": "server updates existing value (bringing c into query)", + "type": "serverUpdate", + "path": "c/index", + "data": null, + "events": [ + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "value": "a" }, + "b": { "value": "b" }, + "c": { "value": "c" } + } + } + ] + }, + { + ".comment": "server updates existing value (sending c out of query)", + "type": "serverUpdate", + "path": "c/index", + "data": 1, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "c", + "data": { "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "value": "a" }, + "b": { "value": "b" } + } + } + ] + } + ] + }, + { + "name": "Reverted writes update query", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": 1 }, + "endAt": { "index": 10 }, + "limitToFirst": 2 + }, + "events": [] + }, + { + ".comment": "update from server sends only query data", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 5, "value": "b" }, + "d": { "index": 6, "value": "d" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 5, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 5, "value": "b" } + } + } + ] + }, + { + ".comment": "user adds new value should update query", + "type": "set", + "path": "", + "data": { + "c": { "index": 2, "value": "c" }, + "a": { "index": 1, "value": "a" } + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "b", + "data": { "index": 5, "value": "b" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "a", + "data": { "index": 2, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "c": { "index": 2, "value": "c" } + } + } + ] + }, + { + ".comment": "write is reverted should revert query to old state", + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "c", + "data": { "index": 2, "value": "c" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 5, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 5, "value": "b" } + } + } + ] + } + ] + }, + { + "name": "Deep set for non-local data doesn't raise events", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "startAt": { "index": 1 }, + "endAt": { "index": 10 }, + "limitToFirst": 2 + }, + "events": [] + }, + { + ".comment": "update from server sends only query data", + "type": "serverUpdate", + "path": "", + "tag": 1, + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 5, "value": "b" } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "index": 1, "value": "a" } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "index": 5, "value": "b" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "b": { "index": 5, "value": "b" } + } + } + ] + }, + { + ".comment": "user updates a value for node outside of query, should trigger no events", + "type": "set", + "path": "c/index", + "data": 1, + "events": [ ] + }, + { + ".comment": "update from server now contains complete data", + "type": "serverMerge", + "path": "", + "tag": 1, + "data": { + "c": { "index": 1, "value": "c" } + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "b", + "data": { "index": 5, "value": "b" } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "a", + "data": { "index": 1, "value": "c" } + }, + { + "path": "", + "type": "value", + "data": { + "a": { "index": 1, "value": "a" }, + "c": { "index": 1, "value": "c" } + } + } + ] + } + ] + }, + { + "name": "User update with new children triggers events", + "steps": + [ + { + "type": "listen", + "path": "", + "params": { + "orderBy": "value", + "tag": 1 + }, + "events": [] + }, + { + ".comment": "update from server sends query data", + "type": "serverUpdate", + "path": "", + "data": { + "a": { "value": 5 }, + "c": { "value": 3 } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": null, + "data": { "value": 3 } + }, + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": "c", + "data": { "value": 5 } + }, + { + "path": "", + "type": "value", + "data": { + "c": { "value": 3 }, + "a": { "value": 5 } + } + } + ] + }, + { + ".comment": "user adds new children through an update", + "type": "update", + "path": "", + "data": { + "b": { "value": 4 }, + "d": { "value": 2 } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "d", + "prevName": null, + "data": { "value": 2 } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "c", + "data": { "value": 4 } + }, + { + "path": "", + "type": "value", + "data": { + "d": { "value": 2 }, + "c": { "value": 3 }, + "b": { "value": 4 }, + "a": { "value": 5 } + } + } + ] + }, + { + ".comment": "server send new server", + "type": "serverMerge", + "path": "", + "data": { + "b": { "value": 4 }, + "d": { "value": 2 } + }, + "events": [] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": false, + "events": [ ] + } + ] + }, + { + "name": "User write with deep user overwrite", + "steps": + [ + { + "type": "listen", + "path": "/foo", + "params": { + "orderBy": "value", + "tag": 1 + }, + "events": [] + }, + { + ".comment": "user sets initial data", + "type": "set", + "path": "/foo", + "data": { + "a": { "value": 1 }, + "b": { "value": 5 }, + "c": { "value": 10 } + }, + "events": [ + { + "path": "/foo", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { "value": 1 } + }, + { + "path": "/foo", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { "value": 5 } + }, + { + "path": "/foo", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { "value": 10 } + }, + { + "path": "/foo", + "type": "value", + "data": { + "a": { "value": 1 }, + "b": { "value": 5 }, + "c": { "value": 10 } + } + } + ] + }, + { + ".comment": "user quickly overwrites value", + "type": "set", + "path": "/foo/c/value", + "data": 3, + "events": [ + { + "path": "/foo", + "type": "child_moved", + "name": "c", + "prevName": "a", + "data": { "value": 3 } + }, + { + "path": "/foo", + "type": "child_changed", + "name": "c", + "prevName": "a", + "data": { "value": 3 } + }, + { + "path": "/foo", + "type": "value", + "data": { + "a": { "value": 1 }, + "c": { "value": 3 }, + "b": { "value": 5 } + } + } + ] + }, + { + ".comment": "server sends complete but outdated data", + "type": "serverUpdate", + "path": "/foo", + "data": { + "a": { "value": 1 }, + "b": { "value": 5 }, + "c": { "value": 10 } + }, + "events": [ ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "events": [ ] + }, + { + ".comment": "server sends update", + "type": "serverUpdate", + "path": "/foo/c/value", + "data": 3, + "events": [ ] + }, + { + "type": "ackUserWrite", + "writeId": 1, + "events": [ ] + } + ] + }, + + { + "name": "Deep server merge", + "steps": [ + { + "type": "listen", + "path": "foo", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "foo": { + "bar1" : { "a": "baz1", "b": "qux1" }, + "bar2" : { "a": "baz2", "b": "qux2" } + } + }, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "bar1", + "prevName": null, + "data": { "a": "baz1", "b": "qux1" } + }, + { + "path": "foo", + "type": "child_added", + "name": "bar2", + "prevName": "bar1", + "data": { "a": "baz2", "b": "qux2" } + }, + { + "path": "foo", + "type": "value", + "data": { + "bar1" : { "a": "baz1", "b": "qux1" }, + "bar2" : { "a": "baz2", "b": "qux2" } + } + } + ] + }, + { + "type": "serverMerge", + "path": "foo", + "data": { + "bar1/a": "newbaz1", + "bar2/b": "newqux2" + }, + "events": [ + { + "path": "foo", + "type": "child_changed", + "name": "bar1", + "prevName": null, + "data": { "a": "newbaz1", "b": "qux1" } + }, + { + "path": "foo", + "type": "child_changed", + "name": "bar2", + "prevName": "bar1", + "data": { "a": "baz2", "b": "newqux2" } + }, + { + "path": "foo", + "type": "value", + "data": { + "bar1" : { "a": "newbaz1", "b": "qux1" }, + "bar2" : { "a": "baz2", "b": "newqux2" } + } + } + ] + } + ] + }, + + { + "name": "Server updates priority", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "listen", + "path": "a/foo", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a", + "data": { "foo": "bar" }, + "events": [ + { + "path": "a/foo", + "type": "value", + "data": "bar" + }, + { + "path": "a", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "a", + "type": "value", + "data": { "foo": "bar" } + } + ] + }, + { + "type": "serverUpdate", + "path": "a/foo/.priority", + "data": "qux", + "events": [ + { + "path": "a/foo", + "type": "value", + "data": { ".value": "bar", ".priority": "qux" } + }, + { + "path": "a", + "type": "child_changed", + "name": "foo", + "prevName": null, + "data": { ".value": "bar", ".priority": "qux" } + }, + { + "path": "a", + "type": "child_moved", + "name": "foo", + "prevName": null, + "data": { ".value": "bar", ".priority": "qux" } + }, + { + "path": "a", + "type": "value", + "data": { + "foo": { ".value": "bar", ".priority": "qux" } + } + } + ] + } + ] + }, + + { + "name": "Revert underlying full overwrite", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "key-a": "val-a", + "key-b": "val-b" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-a", + "prevName": null, + "data": "val-a" + }, + { + "path": "", + "type": "child_added", + "name": "key-b", + "prevName": "key-a", + "data": "val-b" + }, + { + "path": "", + "type": "value", + "data": { + "key-a": "val-a", + "key-b": "val-b" + } + } + ] + }, + { + "type": "set", + "path": "", + "data": { + "key-c": "val-c", + "key-d": "val-d" + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "key-a", + "data": "val-a" + }, + { + "path": "", + "type": "child_removed", + "name": "key-b", + "data": "val-b" + }, + { + "path": "", + "type": "child_added", + "name": "key-c", + "prevName": null, + "data": "val-c" + }, + { + "path": "", + "type": "child_added", + "name": "key-d", + "prevName": "key-c", + "data": "val-d" + }, + { + "path": "", + "type": "value", + "data": { + "key-c": "val-c", + "key-d": "val-d" + } + } + ] + }, + { + "type": "set", + "path": "", + "data": { + "key-e": "val-e", + "key-f": "val-f" + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "key-c", + "data": "val-c" + }, + { + "path": "", + "type": "child_removed", + "name": "key-d", + "data": "val-d" + }, + { + "path": "", + "type": "child_added", + "name": "key-e", + "prevName": null, + "data": "val-e" + }, + { + "path": "", + "type": "child_added", + "name": "key-f", + "prevName": "key-e", + "data": "val-f" + }, + { + "path": "", + "type": "value", + "data": { + "key-e": "val-e", + "key-f": "val-f" + } + } + ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [ ] + } + ] + }, + + { + "name": "User child overwrite for non-existent server node", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "set", + "path": "foo", + "data": { "bar": "qux" }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": { "bar": "qux" } + } + ] + } + ] + }, + + { + "name": "Revert user overwrite of child on leaf node", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": "foo", + "events": [ + { + "path": "", + "type": "value", + "data": "foo" + } + ] + }, + { + "type": "set", + "path": "key", + "data": "value", + "events": [ + { + "path": "", + "type": "child_added", + "name": "key", + "prevName": null, + "data": "value" + }, + { + "path": "", + "type": "value", + "data": { "key": "value" } + } + ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "key", + "prevName": null, + "data": "value" + }, + { + "path": "", + "type": "value", + "data": "foo" + } + ] + } + ] + }, + + { + "name": "Server overwrite with deep user delete", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "key-1": "value-1" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": "value-1" + }, + { + "path": "", + "type": "value", + "data": { + "key-1": "value-1" + } + } + ] + }, + { + ".comment": "User deletes non-existent key, which shouldn't trigger events", + "type": "set", + "path": "key-2/non-key", + "data": null, + "events": [] + }, + { + ".comment": "Server updates node with deep user delete", + "type": "serverUpdate", + "path": "key-2", + "data": { + "deep-key": "deep-value" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-2", + "prevName": "key-1", + "data": { + "deep-key": "deep-value" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": "value-1", + "key-2": { + "deep-key": "deep-value" + } + } + } + ] + } + ] + }, + + { + "name": "User overwrites leaf node with priority", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + ".value": "value", + ".priority": "prio" + }, + "events": [ + { + "path": "", + "type": "value", + "data": { + ".value": "value", + ".priority": "prio" + } + } + ] + }, + { + ".comment": "Overwrite leaf with children node", + "type": "set", + "path": "foo", + "data": "bar", + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "value", + "data": { + ".priority": "prio", + "foo": "bar" + } + } + ] + } + ] + }, + + { + "name": "User overwrites inherit priority values from leaf nodes", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "set", + "path": "", + "data": { + ".value": "value", + ".priority": "prio" + }, + "events": [ + { + "path": "", + "type": "value", + "data": { + ".value": "value", + ".priority": "prio" + } + } + ] + }, + { + ".comment": "user updates the node", + "type": "set", + "path": "foo", + "data": "foo-value", + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "foo-value" + }, + { + "path": "", + "type": "value", + "data": { + ".priority": "prio", + "foo": "foo-value" + } + } + ] + }, + { + ".comment": "The server updates the data for the set", + "type": "serverUpdate", + "path": "", + "data": { + ".value": "value", + ".priority": "prio" + }, + "events": [] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": false, + "events": [] + }, + { + ".comment": "Add another update, should not have old priority", + "type": "set", + "path": "bar", + "data": "bar-value", + "events": [ + { + "path": "", + "type": "child_added", + "name": "bar", + "prevName": null, + "data": "bar-value" + }, + { + "path": "", + "type": "value", + "data": { + ".priority": "prio", + "foo": "foo-value", + "bar": "bar-value" + } + } + ] + } + ] + }, + + { + "name": "User update on user set leaf node with priority after server update", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": null, + "events": [ + { + "path": "", + "type": "value", + "data": null + } + ] + }, + { + "type": "set", + "path": "", + "data": { + ".value": "value", + ".priority": "prio" + }, + "events": [ + { + "path": "", + "type": "value", + "data": { + ".value": "value", + ".priority": "prio" + } + } + ] + }, + { + ".comment": "user overwrite shadows server data", + "type": "serverMerge", + "path": "", + "data": { + "foo": "bar" + }, + "events": [ ] + }, + { + ".comment": "user updates the node", + "type": "update", + "path": "deep/deeper", + "data": { + "0-key": null, + "key": "value" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "deep", + "prevName": null, + "data": { "deeper": { "key": "value" } } + }, + { + "path": "", + "type": "value", + "data": { + ".priority": "prio", + "deep": { "deeper": { "key": "value" } } + } + } + ] + } + ] + }, + + { + "name": "Server deep delete on leaf node", + ".comment": "This is a contrived example, as the server will probably not send null updates to leaf nodes", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": "foo", + "events": [ + { + "path": "", + "type": "value", + "data": "foo" + } + ] + }, + { + ".comment": "this should trigger no events", + "type": "serverUpdate", + "path": "deep/child", + "data": null, + "events": [] + } + ] + }, + + { + "name": "User sets root priority", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "foo": "bar" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "value", + "data": { + "foo": "bar" + } + } + ] + }, + { + "type": "set", + "path": ".priority", + "data": "prio", + "events": [ + { + "path": "", + "type": "value", + "data": { + ".priority": "prio", + "foo": "bar" + } + } + ] + } + ] + }, + + { + "name": "User updates priority on empty root", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + ".comment": "Priority on empty root should not trigger events", + "type": "set", + "path": ".priority", + "data": "prio", + "events": [] + }, + { + ".comment": "This should a value event without priority", + "type": "serverUpdate", + "path": "", + "data": null, + "events": [ + { + "path": "", + "type": "value", + "data": null + } + ] + }, + { + ".comment": "This should now have the user priority", + "type": "serverUpdate", + "path": "", + "data": { + "foo": "bar" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "value", + "data": { + ".priority": "prio", + "foo": "bar" + } + } + ] + } + ] + }, + + { + "name": "Revert set at root with priority", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "foo": "bar" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "value", + "data": { + "foo": "bar" + } + } + ] + }, + { + ".comment": "User overwrites root", + "type": "set", + "path": "", + "data": { + "baz": "qux", + ".priority": "prio" + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "child_added", + "name": "baz", + "prevName": null, + "data": "qux" + }, + { + "path": "", + "type": "value", + "data": { + ".priority": "prio", + "baz": "qux" + } + } + ] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "baz", + "prevName": null, + "data": "qux" + }, + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "value", + "data": { + "foo": "bar" + } + } + ] + } + ] + }, + + { + "name": "Server updates priority after user sets priority", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { ".value": "foo", ".priority": "prio" }, + "events": [ + { + "path": "", + "type": "value", + "data": { ".value": "foo", ".priority": "prio" } + } + ] + }, + { + ".comment": "User overwrites priority", + "type": "set", + "path": ".priority", + "data": "prio-2", + "events": [ + { + "path": "", + "type": "value", + "data": { ".value": "foo", ".priority": "prio-2" } + } + ] + }, + { + ".comment": "this should not trigger any events since a user write is shadowing", + "type": "serverUpdate", + "path": ".priority", + "data": null, + "events": [ ] + } + ] + }, + + { + "name": "User updates priority twice, first is reverted", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { "foo": "bar" }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + }, + { + "path": "", + "type": "value", + "data": { "foo": "bar" } + } + ] + }, + { + ".comment": "User overwrites priority first time", + "type": "set", + "path": ".priority", + "data": "prio-1", + "events": [ + { + "path": "", + "type": "value", + "data": { + "foo": "bar", + ".priority": "prio-1" + } + } + ] + }, + { + ".comment": "User overwrites priority second time", + "type": "set", + "path": ".priority", + "data": "prio-2", + "events": [ + { + "path": "", + "type": "value", + "data": { + "foo": "bar", + ".priority": "prio-2" + } + } + ] + }, + { + ".comment": "revert should not trigger event", + "type": "ackUserWrite", + "writeId": 0, + "revert": true, + "events": [] + }, + { + "type": "serverUpdate", + "path": "foo", + "data": "new-bar", + "events": [ + { + "path": "", + "type": "child_changed", + "name": "foo", + "prevName": null, + "data": "new-bar" + }, + { + "path": "", + "type": "value", + "data": { + "foo": "new-bar", + ".priority": "prio-2" + } + } + ] + } + ] + }, + + { + "name": "Server acks root priority set after user deletes root node", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": "foo", + "events": [ + { + "path": "", + "type": "value", + "data": "foo" + } + ] + }, + { + ".comment": "User overwrites root priority", + "type": "set", + "path": ".priority", + "data": "prio", + "events": [ + { + "path": "", + "type": "value", + "data": { + ".value": "foo", + ".priority": "prio" + } + } + ] + }, + { + ".comment": "User deletes root node", + "type": "set", + "path": "", + "data": null, + "events": [ + { + "path": "", + "type": "value", + "data": null + } + ] + }, + { + "type": "serverUpdate", + "path": ".priority", + "data": "prio", + "events": [] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": false, + "events": [] + } + ] + }, + + { + "name": "A delete in a merge doesn't push out nodes", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "limitToFirst": 3, + "startAt": {"index": null} + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "key-1": 1, + "key-3": 3, + "key-4": 4 + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": 1 + }, + { + "path": "", + "type": "child_added", + "name": "key-3", + "prevName": "key-1", + "data": 3 + }, + { + "path": "", + "type": "child_added", + "name": "key-4", + "prevName": "key-3", + "data": 4 + }, + { + "path": "", + "type": "value", + "data": { + "key-1": 1, + "key-3": 3, + "key-4": 4 + } + } + ] + }, + { + ".comment": "Since key-3 is deleted, key-5 should still remain in the query", + "type": "serverMerge", + "path": "", + "data": { + "key-3": null, + "key-2": 2 + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "key-3", + "data": 3 + }, + { + "path": "", + "type": "child_added", + "name": "key-2", + "prevName": "key-1", + "data": 2 + }, + { + "path": "", + "type": "value", + "data": { + "key-1": 1, + "key-2": 2, + "key-4": 4 + } + } + ] + } + ] + }, + + { + "name": "A tagged query fires events eventually", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "limitToLast": 2 + }, + "events": [] + }, + { + "type": "set", + "path": "", + "data": { + "key-1": 1, + "key-2": 2, + "key-3": 3 + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-2", + "prevName": null, + "data": 2 + }, + { + "path": "", + "type": "child_added", + "name": "key-3", + "prevName": "key-2", + "data": 3 + }, + { + "path": "", + "type": "value", + "data": { + "key-2": 2, + "key-3": 3 + } + } + ] + }, + { + ".comment": "Server updates tagged data, should filter key-1 node", + "type": "serverUpdate", + "path": "", + "tag": 1, + "data": { + "key-2": 2, + "key-3": 3 + }, + "events": [] + }, + { + "type": "ackUserWrite", + "writeId": 0, + "revert": false, + "events": [] + }, + { + ".comment": "User deletes element, only child removed event is fired, since data is not available", + "type": "set", + "path": "key-2", + "data": null, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "key-2", + "data": 2 + }, + { + "path": "", + "type": "value", + "data": { + "key-3": 3 + } + } + ] + }, + { + ".comment": "Server updates tagged data, should filter key-1 node", + "type": "serverMerge", + "path": "", + "tag": 1, + "data": { + "key-1": 1, + "key-2": null + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": 1 + }, + { + "path": "", + "type": "value", + "name": "", + "data": { + "key-1": 1, + "key-3": 3 + } + } + ] + } + ] + }, + + { + "name": "A server update that leaves user sets unchanged is not ignored", + "steps": [ + { + "type": "listen", + "path": "", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "key-1": 1, + "key-2": 2 + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": 1 + }, + { + "path": "", + "type": "child_added", + "name": "key-2", + "prevName": "key-1", + "data": 2 + }, + { + "path": "", + "type": "value", + "data": { + "key-1": 1, + "key-2": 2 + } + } + ] + }, + { + ".comment": "user adds a new node", + "type": "set", + "path": "key-3", + "data": 3, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-3", + "prevName": "key-2", + "data": 3 + }, + { + "path": "", + "type": "value", + "data": { + "key-1": 1, + "key-2": 2, + "key-3": 3 + } + } + ] + }, + { + ".comment": "Server adds new children with full overwrite", + "type": "serverUpdate", + "path": "", + "data": { + "key-1": 1, + "key-2": 2, + "key-4": 4 + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-4", + "prevName": "key-3", + "data": 4 + }, + { + "path": "", + "type": "value", + "data": { + "key-1": 1, + "key-2": 2, + "key-3": 3, + "key-4": 4 + } + } + ] + } + ] + }, + + { + "name": "User write outside of limit is ignored for tagged queries", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "limitToFirst": 2, + "startAt": {"index": null} + }, + "events": [] + }, + { + "type": "serverUpdate", + "tag": 1, + "path": "", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + }, + "key-4": { + "index": 4, + "other-key": "bar" + } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "foo" + } + }, + { + "path": "", + "type": "child_added", + "name": "key-4", + "prevName": "key-1", + "data": { + "index": 4, + "other-key": "bar" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + }, + "key-4": { + "index": 4, + "other-key": "bar" + } + } + } + ] + }, + { + ".comment": "user updates index of child outside, which should bring it in view eventually, but not before the server sends the complete node", + "type": "set", + "path": "key-2/index", + "data": 2, + "events": [] + }, + { + ".comment": "In the meantime the server adds another node", + "type": "serverUpdate", + "tag": 1, + "path": "", + "data": { + "key-1": { + "index": 1, + "other-key": "new-foo" + }, + "key-3": { + "index": 3, + "other-key": "baz" + } + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "key-4", + "data": { + "index": 4, + "other-key": "bar" + } + }, + { + "path": "", + "type": "child_added", + "name": "key-3", + "prevName": "key-1", + "data": { + "index": 3, + "other-key": "baz" + } + }, + { + "path": "", + "type": "child_changed", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "new-foo" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "new-foo" + }, + "key-3": { + "index": 3, + "other-key": "baz" + } + } + } + ] + }, + { + ".comment": "Server now incorperates user update", + "type": "serverUpdate", + "tag": 1, + "path": "key-2", + "data": { + "index": 2, + "other-key": "qux" + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "key-3", + "data": { + "index": 3, + "other-key": "baz" + } + }, + { + "path": "", + "type": "child_added", + "name": "key-2", + "prevName": "key-1", + "data": { + "index": 2, + "other-key": "qux" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "new-foo" + }, + "key-2": { + "index": 2, + "other-key": "qux" + } + } + } + ] + } + ] + }, + + { + "name": "Ack for merge doesn't raise value event for later listen", + "steps": [ + { + "type": "update", + "path": "", + "data": { + "foo": "bar" + }, + "events": [] + }, + { + "type": "listen", + "path": "", + "events": [ + { + "path": "", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": "bar" + } + ] + }, + { + "type": "ackUserWrite", + ".comment": "This acks a merge, so we can't raise a value event yet", + "writeId": 0, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "foo": "bar", + "qux": "quux" + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "qux", + "prevName": "foo", + "data": "quux" + }, + { + "path": "", + "type": "value", + "data": { + "foo": "bar", + "qux": "quux" + } + } + ] + } + ] + }, + + { + "name": "Clear parent shadowing server values merge with server children", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + "type": "serverUpdate", + "path": "a/b", + "data": 2, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 2 + } + ] + }, + { + "type": "update", + "path": "a", + "data": {"b": 28, "c": 3}, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 28 + } + ] + }, + { + ".comment": "This listen should get a complete event snap, as well as complete server children", + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": 28 + }, + { + "path": "a", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": 3 + } + ] + }, + { + ".comment": "Do a serverUpdate with a conflicting value for b, simulates a server value. It's still shadowed though", + "type": "serverUpdate", + "path": "a/b", + "data": 29, + "events": [] + }, + { + ".comment": "Clearing the set should result in updated values for b", + "type": "ackUserWrite", + "writeId": 0, + "events": [ + { + "path": "a/b", + "type": "value", + "data": 29 + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": 29 + } + ] + } + ] + }, + + { + "name": "Priorities don't make me sick", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "set", + "path": "a/foo", + "data": { + "bar": "baz", + ".priority": "prio" + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "foo", + "prevName": null, + "data": { + "bar": "baz", + ".priority": "prio" + } + } + ] + }, + { + "type": "set", + "path": "a/foo/bar", + "data": null, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "foo", + "data": { + "bar": "baz", + ".priority": "prio" + } + } + ] + }, + { + ".comment": "this caused vomitting in the past...", + "type": "set", + "path": "a/foo/bar", + "data": null, + "events": [] + } + ] + }, + + { + "name": "Merge that moves child from window to boundary does not cause child to be readded", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [], + "params": { + "tag": 1, + "limitToFirst": 2, + "startAt": {"index": 1}, + "orderBy": "index" + } + }, + { + "type": "serverUpdate", + "path": "a", + "tag": 1, + "data": { + "2-a": { + "index": 10 + }, + "1-b": { + "index": 20 + } + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "2-a", + "prevName": null, + "data": { + "index": 10 + } + }, + { + "path": "a", + "type": "child_added", + "name": "1-b", + "prevName": "2-a", + "data": { + "index": 20 + } + }, + { + "path": "a", + "type": "value", + "data": { + "2-a": { "index": 10 }, + "1-b": { "index": 20 } + } + } + ] + }, + { + ".comment": "2-a will be the 'next' child after the old '1-b' which will be updated first, but it shouldn't be added because it will actually be out of the window...", + "type": "update", + "path": "a", + "data": { + "1-b": { "index": 0 }, + "2-a": { "index": 30 }, + "3-c": { "index": 5 }, + "4-d": { "index": 6 } + }, + "events": [ + { + "path": "a", + "type": "child_removed", + "name": "2-a", + "data": { "index": 10 } + }, + { + "path": "a", + "type": "child_removed", + "name": "1-b", + "data": { "index": 20 } + }, + { + "path": "a", + "type": "child_added", + "name": "3-c", + "prevName": null, + "data": { "index": 5 } + }, + { + "path": "a", + "type": "child_added", + "name": "4-d", + "prevName": "3-c", + "data": { "index": 6 } + }, + { + "path": "a", + "type": "value", + "data": { + "3-c": { "index": 5 }, + "4-d": { "index": 6 } + } + } + ] + } + ] + }, + + { + "name": "Deep merge ack is handled correctly.", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + ".comment": "Initial server data.", + "type": "serverUpdate", + "path": "a", + "data": null, + "events": [ + { + "path": "a", + "type": "value", + "data": null + } + ] + }, + { + ".comment": "Do deep merge.", + "type": "update", + "path": "a/b", + "data": { + "c": 42, + "d": "hi" + }, + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": { + "c": 42, + "d": "hi" + } + }, + { + "path": "a", + "type": "value", + "data": { + "b": { + "c": 42, + "d": "hi" + } + } + } + ] + }, + { + ".comment": "Server update for our deep merge.", + "type": "serverUpdate", + "path": "a/b", + "data": { + "c": 42, + "d": "hi" + }, + "events": [] + }, + { + ".comment": "ack deep merge.", + "type": "ackUserWrite", + "writeId": 0, + "events": [] + } + ] + }, + + { + "name": "Deep merge ack (on incomplete data, and with server values)", + "steps": [ + { + "type": "listen", + "path": "a/b", + "events": [] + }, + { + ".comment": "Initial server data.", + "type": "serverUpdate", + "path": "a/b", + "data": { + "c": "original-server-value" + }, + "events": [ + { + "path": "a/b", + "type": "child_added", + "name": "c", + "data": "original-server-value", + "prevName": null + }, + { + "path": "a/b", + "type": "value", + "data": { + "c": "original-server-value" + } + } + ] + }, + { + ".comment": "Do deep merge.", + "type": "update", + "path": "a/b", + "data": { + "c": "user-merge-value" + }, + "events": [ + { + "path": "a/b", + "type": "child_changed", + "name": "c", + "data": "user-merge-value", + "prevName": null + }, + { + "path": "a/b", + "type": "value", + "data": { + "c": "user-merge-value" + } + } + ] + }, + { + ".comment": "Listen on a (which won't have complete data).", + "type": "listen", + "path": "a", + "events": [ + { + "path": "a", + "type": "child_added", + "name": "b", + "prevName": null, + "data": { + "c": "user-merge-value" + } + } + ] + }, + { + ".comment": "Server update for our deep merge, but change data (simulate server value).", + "type": "serverUpdate", + "path": "a/b", + "data": { + "c": "user-merge-value-after-server-resolution" + }, + "events": [] + }, + { + ".comment": "ack deep merge.", + "type": "ackUserWrite", + "writeId": 0, + "events": [ + { + "path": "a/b", + "type": "child_changed", + "name": "c", + "data": "user-merge-value-after-server-resolution", + "prevName": null + }, + { + "path": "a/b", + "type": "value", + "data": { + "c": "user-merge-value-after-server-resolution" + } + }, + { + "path": "a", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": { + "c": "user-merge-value-after-server-resolution" + } + } + ] + } + ] + }, + + { + "name": "Limit query handles deep server merge for out-of-view item.", + "steps": [ + { + "type": "listen", + "path": "foo", + "params": { + "tag": 1, + "limitToFirst": 1 + }, + "events": [] + }, + { + ".comment": "Initial server data.", + "type": "serverUpdate", + "path": "foo", + "tag": 1, + "data": { + "a": { + "val": "a-val", + ".priority": "a-pri" + } + }, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { + "val": "a-val", + ".priority": "a-pri" + } + }, + { + "path": "foo", + "type": "value", + "data": { + "a": { + "val": "a-val", + ".priority": "a-pri" + } + } + } + ] + }, + { + ".comment": "Server merge for out-of-view child 'b' (perhaps for another listener). Shouldn't trigger events since we don't have complete data.", + "type": "serverMerge", + "path": "foo/b", + "data": { + "val": "b-val" + }, + "events": [ ] + } + ] + }, + + { + "name": "Limit query handles deep user merge for out-of-view item.", + "steps": [ + { + "type": "listen", + "path": "foo", + "params": { + "tag": 1, + "limitToFirst": 1 + }, + "events": [] + }, + { + ".comment": "Initial server data.", + "type": "serverUpdate", + "path": "foo", + "tag": 1, + "data": { + "a": { + "val": "a-val", + ".priority": "a-pri" + } + }, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { + "val": "a-val", + ".priority": "a-pri" + } + }, + { + "path": "foo", + "type": "value", + "data": { + "a": { + "val": "a-val", + ".priority": "a-pri" + } + } + } + ] + }, + { + ".comment": "User merge for out-of-view child 'b'. Shouldn't trigger events since we don't have complete data.", + "type": "update", + "path": "foo/b", + "data": { + "val": "b-val" + }, + "events": [ ] + } + ] + }, + + { + "name": "Limit query handles deep user merge for out-of-view item followed by server update.", + "steps": [ + { + "type": "listen", + "path": "foo", + "params": { + "tag": 1, + "limitToFirst": 1 + }, + "events": [] + }, + { + ".comment": "Initial server data.", + "type": "serverUpdate", + "path": "foo", + "tag": 1, + "data": { + "a": { + "val": "a-val", + ".priority": "a-pri" + } + }, + "events": [ + { + "path": "foo", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { + "val": "a-val", + ".priority": "a-pri" + } + }, + { + "path": "foo", + "type": "value", + "data": { + "a": { + "val": "a-val", + ".priority": "a-pri" + } + } + } + ] + }, + { + ".comment": "User merge for out-of-view child 'b'. Shouldn't trigger events since we don't have complete data.", + "type": "update", + "path": "foo/b", + "data": { + "val": "b-val-new" + }, + "events": [ ] + }, + { + ".comment": "Server update for 'b', bringing it into view.", + "type": "serverUpdate", + "path": "foo/b", + "data": { + "val": "b-val-old", + "val2": "b-val2" + }, + "events": [ + { + "type": "child_removed", + "path": "foo", + "name": "a", + "prevName": null, + "data": { + "val": "a-val", + ".priority": "a-pri" + } + }, + { + "type": "child_added", + "path": "foo", + "name": "b", + "prevName": null, + "data": { + "val": "b-val-new", + "val2": "b-val2" + } + }, + { + "type": "value", + "path": "foo", + "data": { + "b": { + "val": "b-val-new", + "val2": "b-val2" + } + } + } + ] + } + ] + }, + + { + "name": "Unrelated, untagged update is not cached in tagged listen", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "limitToFirst": 1, + "startAt": {"index": null} + }, + "events": [] + }, + { + "type": "serverUpdate", + "tag": 1, + "path": "", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "foo" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + } + } + } + ] + }, + { + ".comment": "server sends update for key-2 which should not be cached or marked complete", + "type": "serverUpdate", + "path": "key-2", + "data": { + "index": 2, + "other-key": "bar" + }, + "events": [] + }, + { + ".comment": "Now an update for key-1 comes in, marking query as filtered", + "type": "serverMerge", + "tag": 1, + "path": "key-1", + "data": { + "other-key": "new-foo" + }, + "events": [ + { + "path": "", + "type": "child_changed", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "new-foo" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "new-foo" + } + } + } + ] + }, + { + ".comment": "Server now updates node out of view, should not mark view unfiltered", + "type": "serverUpdate", + "path": "key-3", + "data": { "index": 3, "other-key": "qux" }, + "events": [] + }, + { + ".comment": "Server now updates node out of view, should not raise any events", + "type": "serverMerge", + "path": "key-2", + "data": { "index": 0 }, + "events": [] + } + ] + }, + + { + "name": "Unrelated, acked set is not cached in tagged listen", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "limitToFirst": 1, + "startAt": {"index": null} + }, + "events": [] + }, + { + "type": "serverUpdate", + "tag": 1, + "path": "", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "foo" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + } + } + } + ] + }, + { + "type": "set", + "path": "key-1/other-key", + "data": "new-foo", + "events": [ + { + "path": "", + "type": "child_changed", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "new-foo" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "new-foo" + } + } + } + ] + }, + { + "type": "serverUpdate", + "path": "key-1/other-key", + "tag": 1, + "data": "new-foo", + "events": [] + }, + { + ".comment": "The ack should not mark key-2 complete in tagged listen", + "type": "ackUserWrite", + "writeId": 0, + "revert": false, + "events": [] + }, + { + ".comment": "Server now updates node out of view, should not raise any events", + "type": "serverMerge", + "path": "key-2", + "data": { "index": 0 }, + "events": [] + } + ] + }, + + { + "name": "Unrelated, acked update is not cached in tagged listen", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "index", + "limitToFirst": 1, + "startAt": {"index": null} + }, + "events": [] + }, + { + "type": "serverUpdate", + "tag": 1, + "path": "", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "foo" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "foo" + } + } + } + ] + }, + { + "type": "update", + "path": "key-1", + "data": { + "other-key": "new-foo" + }, + "events": [ + { + "path": "", + "type": "child_changed", + "name": "key-1", + "prevName": null, + "data": { + "index": 1, + "other-key": "new-foo" + } + }, + { + "path": "", + "type": "value", + "data": { + "key-1": { + "index": 1, + "other-key": "new-foo" + } + } + } + ] + }, + { + "type": "serverMerge", + "path": "key-1", + "tag": 1, + "data": { + "other-key": "new-foo" + }, + "events": [] + }, + { + ".comment": "The ack should not mark key-2 complete in tagged listen", + "type": "ackUserWrite", + "writeId": 0, + "revert": false, + "events": [] + }, + { + ".comment": "Server now updates node out of view, should not raise any events", + "type": "serverMerge", + "path": "key-2", + "data": { "index": 0 }, + "events": [] + } + ] + }, + { + "name": "Deep update raises immediate events only if has complete data", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "age", + "limitToLast": 1 + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "tag": 1, + "data": { + "a": { + "age": 4 + } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { + "age": 4 + } + }, + { + "path": "", + "type": "value", + "data": { + "a": { + "age": 4 + } + } + } + ] + }, + { + "type": "update", + "path": "", + "data": { + "a/age": 0, + "e": { + "age": 4 + } + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { + "age": 4 + } + }, + { + "path": "", + "type": "child_added", + "name": "e", + "prevName": null, + "data": { + "age": 4 + } + }, + { + "path": "", + "type": "value", + "data": { + "e": { + "age": 4 + } + } + } + ] + }, + { + ".comment": "Now we don't have a full data for child /f, don't raise the event. The events for child /e are correct, although may be confusing for customers.", + "type": "update", + "path": "", + "data": { + "e/age": 0, + "f/age": 4 + }, + "events": [ + { + "path": "", + "type": "child_moved", + "name": "e", + "prevName": null, + "data": { + "age": 0 + } + }, + { + "path": "", + "type": "child_changed", + "name": "e", + "prevName": null, + "data": { + "age": 0 + } + }, + { + "path": "", + "type": "value", + "data": { + "e": { + "age": 0 + } + } + } + ] + }, + { + "type": "serverMerge", + "path": "", + "tag": 1, + "data": { + "f": { + "age": 4 + } + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "e", + "data": { + "age": 0 + } + }, + { + "path": "", + "type": "child_added", + "name": "f", + "prevName": null, + "data": { + "age": 4 + } + }, + { + "path": "", + "type": "value", + "data": { + "f": { + "age": 4 + } + } + } + ] + } + ] + }, + { + "name": "Deep update returns minimum data required", + "steps": [ + { + "type": "listen", + "path": "", + "params": { + "tag": 1, + "orderBy": "idx", + "equalTo": { "index": true } + }, + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "tag": 1, + "data": { + "a": { + "name": "foo", + "idx": true + }, + "b": { + "name": "bar", + "idx": true + } + }, + "events": [ + { + "path": "", + "type": "child_added", + "name": "a", + "prevName": null, + "data": { + "name": "foo", + "idx": true + } + }, + { + "path": "", + "type": "child_added", + "name": "b", + "prevName": "a", + "data": { + "name": "bar", + "idx": true + } + }, + { + "path": "", + "type": "value", + "data": { + "a": { + "name": "foo", + "idx": true + }, + "b": { + "name": "bar", + "idx": true + } + } + } + ] + }, + { + "type": "serverMerge", + "path": "", + "tag": 1, + "data": { + "a/idx": false, + "b/name": "blah", + "c": { + "name": "bar", + "idx": true + } + }, + "events": [ + { + "path": "", + "type": "child_removed", + "name": "a", + "data": { + "name": "foo", + "idx": true + } + }, + { + "path": "", + "type": "child_changed", + "name": "b", + "prevName": null, + "data": { + "name": "blah", + "idx": true + } + }, + { + "path": "", + "type": "child_added", + "name": "c", + "prevName": "b", + "data": { + "name": "bar", + "idx": true + } + }, + { + "path": "", + "type": "value", + "data": { + "b": { + "name": "blah", + "idx": true + }, + "c": { + "name": "bar", + "idx": true + } + } + } + ] + } + ] + }, + { + "name": "Deep update raises all events", + "steps": [ + { + "type": "listen", + "path": "a", + "events": [] + }, + { + "type": "listen", + "path": "b", + "events": [] + }, + { + "type": "serverUpdate", + "path": "", + "data": { + "a": { "aa": 1, "ab": 2 }, + "b": { "ba": 3, "bb": 4 } + }, + "events": [ + { + "path": "a", + "type": "child_added", + "prevName": null, + "name": "aa", + "data": 1 + }, + { + "path": "a", + "type": "child_added", + "prevName": "aa", + "name": "ab", + "data": 2 + }, + { + "path": "a", + "type": "value", + "data": { + "aa": 1, + "ab": 2 + } + }, + { + "path": "b", + "type": "child_added", + "prevName": null, + "name": "ba", + "data": 3 + }, + { + "path": "b", + "type": "child_added", + "prevName": "ba", + "name": "bb", + "data": 4 + }, + { + "path": "b", + "type": "value", + "data": { + "ba": 3, + "bb": 4 + } + } + ] + }, + { + "type": "update", + "path": "", + "data": { + "a/aa": 0, + "b/ba": 0 + }, + "events": [ + { + "path": "a", + "type": "child_changed", + "prevName": null, + "name": "aa", + "data": 0 + }, + { + "path": "a", + "type": "value", + "data": { + "aa": 0, + "ab": 2 + } + }, + { + "path": "b", + "type": "child_changed", + "prevName": null, + "name": "ba", + "data": 0 + }, + { + "path": "b", + "type": "value", + "data": { + "ba": 0, + "bb": 4 + } + } + ] + }, + { + "type": "serverMerge", + "path": "a", + "data": { + "ab/abc": 1, + "ac/acd": 2 + }, + "events": [ + { + "path": "a", + "type": "child_changed", + "prevName": "aa", + "name": "ab", + "data": { "abc": 1 } + }, + { + "path": "a", + "type": "child_added", + "prevName": "ab", + "name": "ac", + "data": { "acd": 2 } + }, + { + "path": "a", + "type": "value", + "data": { + "aa": 0, + "ab": { "abc": 1 }, + "ac": { "acd": 2 } + } + } + ] + } + ] + } +] diff --git a/Example/Database/Tests/third_party/Base64.h b/Example/Database/Tests/third_party/Base64.h new file mode 100644 index 0000000..6db1028 --- /dev/null +++ b/Example/Database/Tests/third_party/Base64.h @@ -0,0 +1,53 @@ +// +// Base64.h +// +// Version 1.1 +// +// Created by Nick Lockwood on 12/01/2012. +// Copyright (C) 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/Base64 +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import <Foundation/Foundation.h> + + +@interface NSData (Base64) + ++ (NSData *)dataWithBase64EncodedString:(NSString *)string; +- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth; +- (NSString *)base64EncodedString; + +@end + + +@interface NSString (Base64) + ++ (NSString *)stringWithBase64EncodedString:(NSString *)string; +- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth; +- (NSString *)base64EncodedString; +- (NSString *)base64DecodedString; +- (NSData *)base64DecodedData; + +@end diff --git a/Example/Database/Tests/third_party/Base64.m b/Example/Database/Tests/third_party/Base64.m new file mode 100644 index 0000000..b3d73db --- /dev/null +++ b/Example/Database/Tests/third_party/Base64.m @@ -0,0 +1,202 @@ +// +// Base64.m +// +// Version 1.1 +// +// Created by Nick Lockwood on 12/01/2012. +// Copyright (C) 2012 Charcoal Design +// +// Distributed under the permissive zlib License +// Get the latest version from here: +// +// https://github.com/nicklockwood/Base64 +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// + +#import "Base64.h" + + +#import <Availability.h> +#if !__has_feature(objc_arc) +#error This library requires automatic reference counting +#endif + + +@implementation NSData (Base64) + ++ (NSData *)dataWithBase64EncodedString:(NSString *)string +{ + const char lookup[] = + { + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99, + 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99, + 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99 + }; + + NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; + long long inputLength = [inputData length]; + const unsigned char *inputBytes = [inputData bytes]; + + long long maxOutputLength = (inputLength / 4 + 1) * 3; + NSMutableData *outputData = [NSMutableData dataWithLength:maxOutputLength]; + unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes]; + + int accumulator = 0; + long long outputLength = 0; + unsigned char accumulated[] = {0, 0, 0, 0}; + for (long long i = 0; i < inputLength; i++) + { + unsigned char decoded = lookup[inputBytes[i] & 0x7F]; + if (decoded != 99) + { + accumulated[accumulator] = decoded; + if (accumulator == 3) + { + outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4); + outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2); + outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3]; + } + accumulator = (accumulator + 1) % 4; + } + } + + //handle left-over data + if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4); + if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2); + if (accumulator > 2) outputLength++; + + //truncate data to match actual output length + outputData.length = outputLength; + return outputLength? outputData: nil; +} + +- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth +{ + //ensure wrapWidth is a multiple of 4 + wrapWidth = (wrapWidth / 4) * 4; + + const char lookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + long long inputLength = [self length]; + const unsigned char *inputBytes = [self bytes]; + + long long maxOutputLength = (inputLength / 3 + 1) * 4; + maxOutputLength += wrapWidth? (maxOutputLength / wrapWidth) * 2: 0; + unsigned char *outputBytes = (unsigned char *)malloc(maxOutputLength); + + long long i; + long long outputLength = 0; + for (i = 0; i < inputLength - 2; i += 3) + { + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; + outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)]; + outputBytes[outputLength++] = lookup[((inputBytes[i + 1] & 0x0F) << 2) | ((inputBytes[i + 2] & 0xC0) >> 6)]; + outputBytes[outputLength++] = lookup[inputBytes[i + 2] & 0x3F]; + + //add line break + if (wrapWidth && (outputLength + 2) % (wrapWidth + 2) == 0) + { + outputBytes[outputLength++] = '\r'; + outputBytes[outputLength++] = '\n'; + } + } + + //handle left-over data + if (i == inputLength - 2) + { + // = terminator + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; + outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)]; + outputBytes[outputLength++] = lookup[(inputBytes[i + 1] & 0x0F) << 2]; + outputBytes[outputLength++] = '='; + } + else if (i == inputLength - 1) + { + // == terminator + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; + outputBytes[outputLength++] = lookup[(inputBytes[i] & 0x03) << 4]; + outputBytes[outputLength++] = '='; + outputBytes[outputLength++] = '='; + } + + if (outputLength >= 4) + { + //truncate data to match actual output length + outputBytes = realloc(outputBytes, outputLength); + return [[NSString alloc] initWithBytesNoCopy:outputBytes + length:outputLength + encoding:NSASCIIStringEncoding + freeWhenDone:YES]; + } + else if (outputBytes) + { + free(outputBytes); + } + return nil; +} + +- (NSString *)base64EncodedString +{ + return [self base64EncodedStringWithWrapWidth:0]; +} + +@end + + +@implementation NSString (Base64) + ++ (NSString *)stringWithBase64EncodedString:(NSString *)string +{ + NSData *data = [NSData dataWithBase64EncodedString:string]; + if (data) + { + return [[self alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } + return nil; +} + +- (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth +{ + NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + return [data base64EncodedStringWithWrapWidth:wrapWidth]; +} + +- (NSString *)base64EncodedString +{ + NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + return [data base64EncodedString]; +} + +- (NSString *)base64DecodedString +{ + return [NSString stringWithBase64EncodedString:self]; +} + +- (NSData *)base64DecodedData +{ + return [NSData dataWithBase64EncodedString:self]; +} + +@end |