/* * 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 "FirebaseDatabase/Tests/Integration/FTransactionTest.h" #import "FirebaseDatabase/Sources/Api/Private/FIRDatabaseQuery_Private.h" #import "FirebaseDatabase/Sources/FIRDatabaseConfig_Private.h" #import "FirebaseDatabase/Tests/Helpers/FEventTester.h" #import "FirebaseDatabase/Tests/Helpers/FTestHelpers.h" #import "FirebaseDatabase/Tests/Helpers/FTupleEventTypeString.h" // HACK used by testUnsentTransactionsAreNotCancelledOnDisconnect to return one bad token and then a // nil token. @interface FIROneBadTokenProvider : NSObject { BOOL firstFetch; } @end @implementation FIROneBadTokenProvider - (instancetype)init { self = [super init]; if (self) { firstFetch = YES; } return self; } - (void)fetchContextForcingRefresh:(BOOL)forceRefresh withCallback:(void (^)(FIRDatabaseConnectionContext *_Nullable context, NSError *_Nullable error))callback { // Simulate delay dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_MSEC)), [FIRDatabaseQuery sharedQueue], ^{ if (self->firstFetch) { self->firstFetch = NO; __auto_type context = [[FIRDatabaseConnectionContext alloc] initWithAuthToken:@"bad-token" appCheckToken:nil]; callback(context, nil); } else { callback(nil, nil); } }); } - (void)listenForAuthTokenChanges:(fbt_void_nsstring)listener { } - (void)listenForAppCheckTokenChanges:(nonnull 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 *updateData = @{ @"foo" : @"newValue", @"boo" : @"newValue", @"doo/foo" : @"newValue", @"loo" : @{@"doo" : @{@"boo" : @"newValue"}} }; [self waitForCompletionOf:ref updateChildValues:updateData]; 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 = [FTestHelpers configForName:@"pending-transactions"]; FIRDatabaseReference *ref = [[[FTestHelpers databaseForConfig:cfg] reference] 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 = [FTestHelpers configForName:configName]; config.contextProvider = [[FIROneBadTokenProvider alloc] init]; // Queue a transaction offline. FIRDatabaseReference *root = [[FTestHelpers databaseForConfig:config] reference]; [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