Files
firebase-ios-sdk/FirebaseDatabase/Tests/Integration/FTransactionTest.m
T
47e0a982fb App check and storage, functions and RTDB support (#7928)
* AppAttestation initial public API

* Run ./scripts/style.sh

* Copyright

* Cleanup

* Cleanup

* Get token forcing refresh method

* Formatting

* Forcing refresh samples

* Interop introduced

* Code samples for Interop

* Run  ./scripts/style.sh

* Cleanup

* TODOs and Copirights

* Fix build

* Supported iOS versions

* Remove unnecessary bridging header

* Run  ./scripts/style.sh

* [WIP] DeviceCheckAttestationProvider as a subspec

* cleanup

* Add memory attributes to readonly properties

* Move FIRDeviceCheckAttestationProvider.h to private headers

* TODO for `FirebaseAppAttestation/Core` name

* Remove memory attributes from FIRAppAttestationToken properties

* FIRAppAttestationTests.testSetAttestationProviderFactory introduced

* FIRAppAttestationToken initializer

* Basic FIRLibrary implementation and FIRAppAttestationProviderFactory ingestion.

* Fix

* FIRAppAttestationToken+Interop.h

* TODOs

* Tests: reset FIRApps

* GHA for App Attestation

* Run ./scripts/style.sh

* Style fixes.

* Run ./scripts/style.sh

* TODO

* Nullable initializer and type annpotations

* TODOs

* TODOs

* TODO

* Fix spec and allow warnings

* AppAttestation initial public API

* Run ./scripts/style.sh

* Copyright

* Cleanup

* Cleanup

* Get token forcing refresh method

* Formatting

* Forcing refresh samples

* Interop introduced

* Code samples for Interop

* Run  ./scripts/style.sh

* Cleanup

* TODOs and Copirights

* Fix build

* Supported iOS versions

* Remove unnecessary bridging header

* Run  ./scripts/style.sh

* [WIP] DeviceCheckAttestationProvider as a subspec

* cleanup

* Add memory attributes to readonly properties

* Move FIRDeviceCheckAttestationProvider.h to private headers

* TODO for `FirebaseAppAttestation/Core` name

* Remove memory attributes from FIRAppAttestationToken properties

* FIRAppAttestationTests.testSetAttestationProviderFactory introduced

* FIRAppAttestationToken initializer

* Basic FIRLibrary implementation and FIRAppAttestationProviderFactory ingestion.

* Fix

* FIRAppAttestationToken+Interop.h

* TODOs

* Tests: reset FIRApps

* Run ./scripts/style.sh

* Style fixes.

* Run ./scripts/style.sh

* TODO

* Nullable initializer and type annpotations

* TODOs

* TODOs

* TODO

* Revert FirestoreDecoder.swift formatting.

* Basic implementation of FIRDeviceCheckAttestationProvider and tests (#626)

* DeviceCheck attestation provider sceleton

* DeviceCheck attestation provider sceleton

* Basic FIRDeviceCheckAttestationProviderTests

* Run ./scripts/style.sh

* FIRAppAttestStorage implementation and tests (#627)

* FIRDeviceCheckAttestationProvider designated init

* FIRAppAttestStorage API and basic tests.

* FIRAppAttestStoredToken: NSSecureCoding implementation

* WIP Storage

* Add `accessGroup` param to `FIRAppAttestStorage`

* FIRAppAttestStorage implementation

* Use constants

* FIRAppAttestStorage API doc

* Coding style and typos.

* Code formatting

* Attestation Token caching (#630)

* Storage instance and looging

* AppAttestation caching

* Tests and cleanup

* Cleanup

* Rename App Attestation -> App Check (#631)

* Rename App Attestation -> App Check

* Run ./scripts/style.sh

* Update GHA

* Default Custom Provider (#633)

* AppCheck: remove not unnecessary public API (#634)

* AppCheck: remove not unnecessary public API

* Run ./scripts/style.sh

* AppCheck: DeviceCheck token server API (#635)

* Generic HTTP request handling

* [draft] AppCheck: device check HTTP API and simple tests

* Parse response

* Cleanup

* AppCheck: API errors and tests (#636)

* AppCheck errors

* API service tests and fixes

* Cleanup

* Run ./scripts/style.sh

* Naming

* AppCheck: Register device check provider factory as a default (#637)

* AppCheck errors

* API service tests and fixes

* Cleanup

* Run ./scripts/style.sh

* AppCheck: register FIRDeviceCheckProvider as a component

* Naming

* Registre device check provider factory as a default

* FIRDeviceCheckProvider tests

* AppCheck: API communication common for all providers moved to FIRAppCheckAPIService (#640)

* FIRAppCheckAPIService introduced

* Move URL seession stub to FIRURLSessionOCMockStub

* Run ./scripts/style.sh

* Move approximate date comparing to FIRDateTestUtils

* compose request in FIRAppCheckAPIService

* FIRAppCheckAPIServiceTests

* Use FIRAppCheckAPIService in FIRDeviceCheckProvider

* Cleanup

* DeviceCheck mock server integration and tests (#686)

* DeviceCheck mock server integration and tests

* TODO

* Update FIROptions.h import

* Temporary add FirebaseAppCheck to skip import validation

* TestUtilities moved to SharedTestUtilities

* FirebaseAppCheck.podspec:  TestUtilities->SharedTestUtilities

* SwiftPM: add OCMock dependency to SharedTestUtilities

* FIRURLSessionOCMockStub: #import "OCMock.h"

* App Check: Firebase 7 fixes (#715)

* AppCheck Firebase 7

* FirebaseCore repo-relative imports

* Fix FIRAppCheckStorage.m compilation

* Repo relative imports

* linebreak

* AppCheck: debug FAC token provider (#716)

* AppCheck Firebase 7

* FirebaseCore repo-relative imports

* Fix FIRAppCheckStorage.m compilation

* Repo relative imports

* linebreak

* Remove FIRAppCheckDefaultCustomProvider

* FIRAppCheckDebugProvider introduced

* FIRAppCheckDebugProviderTests

* style

* Print locally generated debug token to console

* Fix imports

* mullability and line breaks

* typo

* Comments

* TODO

* AppCheck: merge into single spec (package) (#717)

* Standard package folder structure

* Move interop header into AppCheck sources folder

* Combine public headers in the standard dir

* Private headers to internal

* Podspec

* Headers import udpdate

* style

* Fix header imports

* Enable check_imports.swift and fix imports

* AppCheck: server request adjustments (#718)

* Wrap DeviceCheck token into JSON

* AppChek - remove static_framework requirement

* WIP

* Update default base URL

* Set content-type header

* Check for empty response data

* Fix tests

* Style and formatting

* Podspec - revert wrong change and remove static_framework

* Assign error to error pointer safely.

* AppCheck: primitive test app (#719)

* Remove static_framework requirement

* WIP: FIRAppCheckTestApp introduced

* Delete .plist

* Copyright

* Remove empty test targets

* pod deintegrate

* Remove test targets from Podfile

* formatting

* Storage: integration with AppCheck (#720)

* Storage: add AppCheck interop dependency

* Inject AppCheck into FIRStorageTokenAuthorizer

* initial AppCheck integration

* Introduce AppCheck fake

* AppCheck fake implementation

* FIRStorageTokenAuthorizer tests and fixes

* @"Authorization" header constant

* Log AppCheck error.

* Cleanup

* FIRStorageTokenAuthorizerTests: helpers

* TODO

* Cleanup

* AppCheck: separate 2P and 3P API (#724)

* FIRAppCheckInterop: remove a convenience method.

* Rename: FIRAppCheckTokenInterop->FIRAppCheckTokenResultInterop

* FIRAppCheckTokenResult introduced

* Update FIRAppCheckTokenHandlerInterop,  FIRAppCheckTokenResultInterop and tests

* style

* Update token result

* Update storage integration

* Adjust check_imports.swift for test files

* Don't use internal AppCheck classes in Storage tests

* style

* Rename token to tokenResult in 2P API

* Add private Core headers to app check sources

* Fix storage log message code format

* token clarification comments in API docs

* Rename token->tokenResult

* AppCheck cleanup (#737)

* Fix public headers import

* Cleanup imported test utils

* FirebaseAppCheck podspec: revert git URL

* AppCheck: Debug token to AppCheck token exchange API (#739)

* GUL: make NSURLSession promise extension public

* Use NSURLSession+GULPromises to send requests

* style

* Move FIRAppCheckToken+APIResponse to Core dir

* rename

* FIRAppCheckDebugProviderAPIService introduced

* Move async token parsing API to FIRAppCheckToken extension

* Move token exchange response handling to FIRAppCheckAPIService. Tests WIP.

* Move API response parsing tests to FIRAppCheckAPIServiceTests

* FIRAppCheckDebugProviderAPIServiceTests

* Cleanup

* style

* App Check debug provider: integrate with exchange API (#743)

* Move FIROption validation to FIRAppCheckValidator

* FIRAppCheckDebugProvider token exchange and tests

* Fix FIRAppCheckDebugProviderFactoryTests

* style

* App Check test app debug provider fix (#747)

* App Check: minor touchups (#750)

* Update dummy token value

* Print local debug token to logs

* Require GoogleUtilities v7.2 and above

* Print debug token on init

* Don't print debug token again

* App Check: use prod environment (#751)

* App Check: use prod backend by default.

* Update token expiration threshold

* App Check SwiftPM support (#753)

* AppCheck SwiftPM package definition

* Promise header import fix

* OCMock header fix

* Fix fixture dependent tests

* Exclude Keychain dependent tests

* License

* ./scripts/style.sh

* Cleanup

* Format with proper SwiftFormat version

* `.process` resources.

* Swift tests target

* Cleanup and code formatting

* App Check: token auto-refresh (#754)

* FIRAppCheckTokenRefresher API defined

* FIRAppCheckTokenRefresher implementation draft

* [WIP] Token refresher integration

* Token refresher integration

* FIRAppCheckTests: token refresher tests

* FIRAppCheckTests: additional tests cases

* FIRAppCheckTimer introduced

* FIRAppCheckTokenRefresher: use FIRAppCheckTimer instead of the dispatch timer.

* FIRFakeTimer

* FIRAppCheckTokenRefresherTests testInitialRefresh

* FIRAppCheckTokenRefresherTests: testNextRefreshOnRefreshSuccess

* ./scripts/style.sh

* Exponential backoff

* Style and comments

* Move token refresh files to a separate folder

* FIRAppCheckTimerTests

* FIRAppCheck: use expicit class in init

* FIRAppCheckDebugProviderFactory Swift name

* FIRAppCheckTokenRefresher: Increment/decrement directly

* App Check fixes (#755)

* Fix backoff: no backoff for the initial request

* Use eager initialization

* FIRAppCheckTokenRefresher: don't refresh until handler set

* FIRAppCheckIntegrationTests: disable token refresher

* Fix testInitialRefresh test

* style

* FIRAppCheckIntegrationTests fix

* Temporary exclude App Check from platfrom logging (#756)

* App Check: send app check SDK version only in the header (#757)

* App Check: store FAC token per app ID (#758)

* App Check: store FAC token per app ID

* update podspec

* API docs

* FIRAppCheck initWithApp tests

* AppCheckCustomProvideApp (#760)

* Functions: App Check integration (#762)

* Functions: app check API added to FUNContextProvider

* FUNContextProviderTests

* FUNContextProvider app check support

* ./scripts/style.sh

* FIRFunctions: pass FAC token as HTTP header

* Fix SPM

* Fix Firebase-Package scheme bad merge

* Cleanup

* RTDB: pass FAC token to web socket (#763)

* RTDB: pass FAC token to web socket

* FIRAppCheckFake default init

* Rename FAuthTokenProvider to FIRDatabaseConnectionContext; integrate with App Check

* Pass App Check token in the HTTP header

* Rename

* Fix existing tests

* TODO

* ./scripts/style.sh

* Context provider: assume separate FAC token change listener

* FIRDatabaseConnectionContextProviderTests

* Remove unused files

* Copy NSMutableURLRequest

* Cleanup

* ./scripts/style.sh

* FIRDatabaseConnectionContextProvider: sync callback when no auth or app check

* Cleanup

* Rename "authToken" leftovers to "context"

* Fix renaming

* Pass missing headers to NSURLSessionWebSocketTask

* Cleanup

* add a name to TODO

* App Check: token update notifications (#764)

* AppCheck: token did change API and tests

* notification implementation and fixes

* Run ./scripts/style.sh

* Update comments

* RTDB: send app check token updates to keep connection open (#765)

* Pass AppCheck instance as an object into the the FAC token update notification

* FIRDatabaseConnectionContext: add FAC token change listener API

* formatting

* Send FAC token updates to open connection

* More informative empty response message

* refreshAppCheckToken fixes

* Remove assertion

* Don't modify state

* forceTokensRefresh -> forceTokenRefreshes

* Call listeners on [FIRDatabaseQuery sharedQueue]

* App Check API cleanup and docs (#766)

* AppCheck public API and docs cleanup

* More API docs

* Fix compilation errors

* typos and wording

* Make summary different from description (pod lib lint warning)

* port app check workflow changes from another branch

* pod lib lint: remove --allow-warnings

* App Check cleanup (#767)

* Cleanup debug code and outdated TODOs

* Fix FIRDeviceCheckProvider availablility for SPM

* Fix availability in tests

* Fix availability in tests

* Make sure os requirements are consistent for tests specs

* Migrate v1alpha URLs to v1beta

* Migrate v1alpha URLs to v1beta

* Run ./scripts/style.sh

* Functions tests: fix Xcode 11 warnings (#7931)

* Functions tests: fix Xcode 11 warnings

* style

* App Check: token auto-refresh opt-out API (#769)

* FIRAppCheckSettings introduced (WIP)

* FIRAppCheckSettingsTests

* Style

* Make FIRAppCheckSettings instance per-app.

* FIRAppCheckTokenRefresher: use FIRAppCheckSettings.isTokenAutoRefreshEnabled (WIP)

* Tests

* Add public instance getters

* Fix analyzer warnings

* Fix testspec OS versions

* FIRAppCheckTokenRefresher expiration date update; tests

* FIRAppCheck.isTokenAutoRefreshEnabled implementation and tests

* style

* FIRAppCheck: Use specific types for variables in the default init

* More precise comments

* missing symbol

* FIRAppCheckSettings: remove defensive conditions from init.

Co-authored-by: Nick Cooke <nickcooke@google.com>
Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com>
2021-04-21 21:01:11 -04:00

1546 lines
53 KiB
Objective-C

/*
* 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 <FIRDatabaseConnectionContextProvider> {
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 *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 = [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