/* * Copyright 2017 Google LLC * * 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 "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" #import #import #import #import #import #import #import #import #import #import #include #include #include #include #import "FirebaseCore/Extension/FIRAppInternal.h" #import "FirebaseCore/Extension/FIRLogger.h" #import "FirebaseCore/Extension/FIROptionsInternal.h" #import "Firestore/Example/Tests/Util/FIRFirestore+Testing.h" #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Source/API/FIRAggregateQuery+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #include "Firestore/core/src/credentials/credentials_provider.h" #include "Firestore/core/src/credentials/empty_credentials_provider.h" #include "Firestore/core/src/credentials/user.h" #include "Firestore/core/src/local/leveldb_opener.h" #include "Firestore/core/src/model/database_id.h" #include "Firestore/core/src/remote/firebase_metadata_provider_apple.h" #include "Firestore/core/src/remote/grpc_connection.h" #include "Firestore/core/src/util/async_queue.h" #include "Firestore/core/src/util/autoid.h" #include "Firestore/core/src/util/filesystem.h" #include "Firestore/core/src/util/path.h" #include "Firestore/core/src/util/string_apple.h" #include "Firestore/core/test/unit/testutil/app_testing.h" #include "Firestore/core/test/unit/testutil/async_testing.h" #include "Firestore/core/test/unit/testutil/status_testing.h" #include "absl/memory/memory.h" using firebase::firestore::core::DatabaseInfo; using firebase::firestore::credentials::CredentialChangeListener; using firebase::firestore::credentials::EmptyAppCheckCredentialsProvider; using firebase::firestore::credentials::EmptyAuthCredentialsProvider; using firebase::firestore::credentials::User; using firebase::firestore::local::LevelDbOpener; using firebase::firestore::model::DatabaseId; using firebase::firestore::remote::FirebaseMetadataProviderApple; using firebase::firestore::testutil::AppForUnitTesting; using firebase::firestore::testutil::AsyncQueueForTesting; using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::CreateAutoId; using firebase::firestore::util::Filesystem; using firebase::firestore::util::MakeString; using firebase::firestore::util::Path; using firebase::firestore::util::Status; using firebase::firestore::util::StatusOr; NS_ASSUME_NONNULL_BEGIN /** * Firestore databases can be subject to a ~30s "cold start" delay if they have not been used * recently, so before any tests run we "prime" the backend. */ static const double kPrimingTimeout = 45.0; static NSString *defaultProjectId; static FIRFirestoreSettings *defaultSettings; static bool runningAgainstEmulator = false; // Behaves the same as `EmptyCredentialsProvider` except it can also trigger a user // change. class FakeAuthCredentialsProvider : public EmptyAuthCredentialsProvider { public: void SetCredentialChangeListener(CredentialChangeListener changeListener) override { if (changeListener) { listener_ = std::move(changeListener); listener_(User::Unauthenticated()); } } void ChangeUser(NSString *new_id) { if (listener_) { listener_(firebase::firestore::credentials::User::FromUid(new_id)); } } private: CredentialChangeListener listener_; }; @implementation FSTIntegrationTestCase { NSMutableArray *_firestores; std::shared_ptr _fakeAppCheckCredentialsProvider; std::shared_ptr _fakeAuthCredentialsProvider; } - (void)setUp { [super setUp]; LoadXCTestCaseAwait(); _fakeAppCheckCredentialsProvider = std::make_shared(); _fakeAuthCredentialsProvider = std::make_shared(); [self clearPersistenceOnce]; [self primeBackend]; _firestores = [NSMutableArray array]; self.db = [self firestore]; self.eventAccumulator = [FSTEventAccumulator accumulatorForTest:self]; } - (void)tearDown { @try { for (FIRFirestore *firestore in _firestores) { [self terminateFirestore:firestore]; } } @finally { _firestores = nil; [super tearDown]; } } /** * Clears persistence, but only the first time. This ensures that each test * run is isolated from the last test run, but doesn't allow tests to interfere * with each other. */ - (void)clearPersistenceOnce { auto *fs = Filesystem::Default(); static bool clearedPersistence = false; @synchronized([FSTIntegrationTestCase class]) { if (clearedPersistence) return; DatabaseInfo dbInfo; LevelDbOpener opener(dbInfo); StatusOr maybeLevelDBDir = opener.FirestoreAppDataDir(); ASSERT_OK(maybeLevelDBDir.status()); Path levelDBDir = std::move(maybeLevelDBDir).ValueOrDie(); Status status = fs->RecursivelyRemove(levelDBDir); ASSERT_OK(status); clearedPersistence = true; } } - (FIRFirestore *)firestore { return [self firestoreWithProjectID:[FSTIntegrationTestCase projectID]]; } /** * Figures out what kind of testing environment we're using, and sets up testing defaults to make * that work. * * Several configurations are supported: * * Mobile Harness, running periocally against prod and nightly, using live SSL certs * * Firestore emulator, running on localhost, with SSL disabled * * See Firestore/README.md for detailed setup instructions or comments below for which specific * values trigger which configurations. */ + (void)setUpDefaults { if (defaultSettings) return; defaultSettings = [[FIRFirestoreSettings alloc] init]; // Check for a MobileHarness configuration, running against nightly or prod, which have live // SSL certs. NSString *project = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"]; NSString *targetBackend = [[NSProcessInfo processInfo] environment][@"TARGET_BACKEND"]; NSString *host; if (targetBackend) { if ([targetBackend isEqualToString:@"emulator"]) { [self setUpEmulatorDefault]; return; } else if ([targetBackend isEqualToString:@"qa"]) { host = @"staging-firestore.sandbox.googleapis.com"; } else if ([targetBackend isEqualToString:@"nightly"]) { host = @"test-firestore.sandbox.googleapis.com"; } else if ([targetBackend isEqualToString:@"prod"]) { host = @"firestore.googleapis.com"; } else { @throw [[NSException alloc] initWithName:@"InvalidArgumentError" reason:[NSString stringWithFormat: @"Unexpected TARGET_BACKEND environment variable \"%@\"", targetBackend] userInfo:nil]; } } else { host = [[NSProcessInfo processInfo] environment][@"DATASTORE_HOST"]; } if (project && host) { defaultProjectId = project; defaultSettings.host = host; NSLog(@"Integration tests running against %@/%@", defaultSettings.host, defaultProjectId); return; } // Check for configuration of a prod project via GoogleServices-Info.plist. FIROptions *options = [FIROptions defaultOptions]; if (options && ![options.projectID isEqualToString:@"abc-xyz-123"]) { defaultProjectId = options.projectID; if (host) { // Allow access to nightly or other hosts via this mechanism too. defaultSettings.host = host; } NSLog(@"Integration tests running against %@/%@", defaultSettings.host, defaultProjectId); return; } // Otherwise fall back on assuming the emulator or localhost. [self setUpEmulatorDefault]; } + (void)setUpEmulatorDefault { defaultProjectId = @"test-db"; defaultSettings.host = @"localhost:8080"; defaultSettings.sslEnabled = false; runningAgainstEmulator = true; NSLog(@"Integration tests running against the emulator at %@/%@", defaultSettings.host, defaultProjectId); } + (NSString *)projectID { if (!defaultProjectId) { [self setUpDefaults]; } return defaultProjectId; } + (bool)isRunningAgainstEmulator { // The only way to determine whether or not we're running against the emulator is to figure out // which testing environment we're using. Essentially `setUpDefaults` determines // `runningAgainstEmulator` as a side effect. if (!defaultProjectId) { [self setUpDefaults]; } return runningAgainstEmulator; } + (FIRFirestoreSettings *)settings { [self setUpDefaults]; return defaultSettings; } - (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID { FIRApp *app = AppForUnitTesting(MakeString(projectID)); return [self firestoreWithApp:app]; } - (FIRFirestore *)firestoreWithApp:(FIRApp *)app { NSString *persistenceKey = [NSString stringWithFormat:@"db%lu", (unsigned long)_firestores.count]; FIRSetLoggerLevel(FIRLoggerLevelDebug); std::string projectID = MakeString(app.options.projectID); FIRFirestore *firestore = [[FIRFirestore alloc] initWithDatabaseID:DatabaseId(projectID) persistenceKey:MakeString(persistenceKey) authCredentialsProvider:_fakeAuthCredentialsProvider appCheckCredentialsProvider:_fakeAppCheckCredentialsProvider workerQueue:AsyncQueueForTesting() firebaseMetadataProvider:absl::make_unique(app) firebaseApp:app instanceRegistry:nil]; firestore.settings = [FSTIntegrationTestCase settings]; [_firestores addObject:firestore]; return firestore; } - (void)triggerUserChangeWithUid:(NSString *)uid { _fakeAuthCredentialsProvider->ChangeUser(uid); } - (void)primeBackend { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [FSTIntegrationTestCase setUpDefaults]; if (runningAgainstEmulator) { // Priming not required against the emulator. return; } FIRFirestore *db = [self firestore]; XCTestExpectation *watchInitialized = [self expectationWithDescription:@"Prime backend: Watch initialized"]; __block XCTestExpectation *watchUpdateReceived; FIRDocumentReference *docRef = [db documentWithPath:[self documentPath]]; id listenerRegistration = [docRef addSnapshotListener:^(FIRDocumentSnapshot *snapshot, NSError *) { if ([snapshot[@"value"] isEqual:@"done"]) { [watchUpdateReceived fulfill]; } else { [watchInitialized fulfill]; } }]; // Wait for watch to initialize and deliver first event. [self awaitExpectation:watchInitialized]; watchUpdateReceived = [self expectationWithDescription:@"Prime backend: Watch update received"]; // Use a transaction to perform a write without triggering any local events. [docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **) { [transaction setData:@{@"value" : @"done"} forDocument:docRef]; return nil; } completion:^(id, NSError *){ }]; // Wait to see the write on the watch stream. [self waitForExpectationsWithTimeout:kPrimingTimeout handler:^(NSError *_Nullable expectationError) { if (expectationError) { XCTFail(@"Error waiting for prime backend: %@", expectationError); } }]; [listenerRegistration remove]; [self terminateFirestore:db]; }); } - (void)terminateFirestore:(FIRFirestore *)firestore { XCTestExpectation *expectation = [self expectationWithDescription:@"shutdown"]; [firestore terminateWithCompletion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (void)deleteApp:(FIRApp *)app { XCTestExpectation *expectation = [self expectationWithDescription:@"deleteApp"]; [app deleteApp:^(BOOL completion) { XCTAssertTrue(completion); [expectation fulfill]; }]; [self awaitExpectation:expectation]; } - (NSString *)documentPath { std::string autoId = CreateAutoId(); return [NSString stringWithFormat:@"test-collection/%s", autoId.c_str()]; } - (FIRDocumentReference *)documentRef { return [self.db documentWithPath:[self documentPath]]; } - (FIRCollectionReference *)collectionRef { std::string autoId = CreateAutoId(); NSString *collectionName = [NSString stringWithFormat:@"test-collection-%s", autoId.c_str()]; return [self.db collectionWithPath:collectionName]; } - (FIRCollectionReference *)collectionRefWithDocuments: (NSDictionary *> *)documents { FIRCollectionReference *collection = [self collectionRef]; // Use a different instance to write the documents [self writeAllDocuments:documents toCollection:[[self firestore] collectionWithPath:collection.path]]; return collection; } - (void)writeAllDocuments:(NSDictionary *> *)documents toCollection:(FIRCollectionReference *)collection { NSMutableArray *commits = [[NSMutableArray alloc] init]; __block FIRWriteBatch *writeBatch = nil; __block int writeBatchSize = 0; [documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary *value, BOOL *) { if (writeBatch == nil) { writeBatch = [collection.firestore batch]; } [writeBatch setData:value forDocument:[collection documentWithPath:key]]; writeBatchSize++; // Write batches are capped at 500 writes. Use 400 just to be safe. if (writeBatchSize == 400) { XCTestExpectation *commitExpectation = [self expectationWithDescription:@"WriteBatch commit"]; [writeBatch commitWithCompletion:^(NSError *_Nullable error) { [commitExpectation fulfill]; if (error != nil) { XCTFail(@"WriteBatch commit failed: %@", error); } }]; [commits addObject:commitExpectation]; writeBatch = nil; writeBatchSize = 0; } }]; if (writeBatch != nil) { XCTestExpectation *commitExpectation = [self expectationWithDescription:@"WriteBatch commit"]; [writeBatch commitWithCompletion:^(NSError *_Nullable error) { [commitExpectation fulfill]; if (error != nil) { XCTFail(@"WriteBatch commit failed: %@", error); } }]; [commits addObject:commitExpectation]; } for (XCTestExpectation *commitExpectation in commits) { [self awaitExpectation:commitExpectation]; } } - (void)readerAndWriterOnDocumentRef:(void (^)(FIRDocumentReference *readerRef, FIRDocumentReference *writerRef))action { FIRFirestore *reader = self.db; // for clarity FIRFirestore *writer = [self firestore]; NSString *path = [self documentPath]; FIRDocumentReference *readerRef = [reader documentWithPath:path]; FIRDocumentReference *writerRef = [writer documentWithPath:path]; action(readerRef, writerRef); } - (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref { return [self readDocumentForRef:ref source:FIRFirestoreSourceDefault]; } - (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref source:(FIRFirestoreSource)source { __block FIRDocumentSnapshot *result; XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; [ref getDocumentWithSource:source completion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) { XCTAssertNil(error); result = doc; [expectation fulfill]; }]; [self awaitExpectation:expectation]; return result; } - (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query { return [self readDocumentSetForRef:query source:FIRFirestoreSourceDefault]; } - (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query source:(FIRFirestoreSource)source { if (query == nil) { XCTFail("Trying to read data from a nil query"); } __block FIRQuerySnapshot *result; XCTestExpectation *expectation = [self expectationWithDescription:@"getData"]; [query getDocumentsWithSource:source completion:^(FIRQuerySnapshot *documentSet, NSError *error) { XCTAssertNil(error); result = documentSet; [expectation fulfill]; }]; [self awaitExpectation:expectation]; return result; } - (FIRDocumentSnapshot *)readSnapshotForRef:(FIRDocumentReference *)ref requireOnline:(BOOL)requireOnline { __block FIRDocumentSnapshot *result; XCTestExpectation *expectation = [self expectationWithDescription:@"listener"]; id listener = [ref addSnapshotListenerWithIncludeMetadataChanges:YES listener:^(FIRDocumentSnapshot *snapshot, NSError *error) { XCTAssertNil(error); if (!requireOnline || !snapshot.metadata.fromCache) { result = snapshot; [expectation fulfill]; } }]; [self awaitExpectation:expectation]; [listener remove]; return result; } - (FIRAggregateQuerySnapshot *)readSnapshotForAggregate:(FIRAggregateQuery *)query { __block FIRAggregateQuerySnapshot *result; XCTestExpectation *expectation = [self expectationWithDescription:@"aggregate result"]; [query aggregationWithSource:FIRAggregateSourceServer completion:^(FIRAggregateQuerySnapshot *snapshot, NSError *error) { XCTAssertNil(error); result = snapshot; [expectation fulfill]; }]; [self awaitExpectation:expectation]; return result; } - (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data { XCTestExpectation *expectation = [self expectationWithDescription:@"setData"]; [ref setData:data completion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data { XCTestExpectation *expectation = [self expectationWithDescription:@"updateData"]; [ref updateData:data completion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (void)deleteDocumentRef:(FIRDocumentReference *)ref { XCTestExpectation *expectation = [self expectationWithDescription:@"deleteDocument"]; [ref deleteDocumentWithCompletion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (FIRDocumentReference *)addDocumentRef:(FIRCollectionReference *)ref data:(NSDictionary *)data { XCTestExpectation *expectation = [self expectationWithDescription:@"addDocument"]; FIRDocumentReference *doc = [ref addDocumentWithData:data completion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; return doc; } - (void)mergeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data { XCTestExpectation *expectation = [self expectationWithDescription:@"setDataWithMerge"]; [ref setData:data merge:YES completion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (void)mergeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary *)data fields:(NSArray *)fields { XCTestExpectation *expectation = [self expectationWithDescription:@"setDataWithMerge"]; [ref setData:data mergeFields:fields completion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (void)disableNetwork { XCTestExpectation *expectation = [self expectationWithDescription:@"disableNetwork"]; [self.db disableNetworkWithCompletion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (void)enableNetwork { XCTestExpectation *expectation = [self expectationWithDescription:@"enableNetwork"]; [self.db enableNetworkWithCompletion:[self completionForExpectation:expectation]]; [self awaitExpectation:expectation]; } - (const std::shared_ptr &)queueForFirestore:(FIRFirestore *)firestore { return [firestore workerQueue]; } - (void)waitUntil:(BOOL (^)())predicate { NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; double waitSeconds = [self defaultExpectationWaitSeconds]; while (!predicate() && ([NSDate timeIntervalSinceReferenceDate] - start < waitSeconds)) { // This waits for the next event or until the 100ms timeout is reached [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; } if (!predicate()) { XCTFail(@"Timeout"); } } extern "C" NSArray *> *FIRQuerySnapshotGetData( FIRQuerySnapshot *docs) { NSMutableArray *> *result = [NSMutableArray array]; for (FIRDocumentSnapshot *doc in docs.documents) { [result addObject:doc.data]; } return result; } extern "C" NSArray *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) { NSMutableArray *result = [NSMutableArray array]; for (FIRDocumentSnapshot *doc in docs.documents) { [result addObject:doc.documentID]; } return result; } extern "C" NSArray *> *FIRQuerySnapshotGetDocChangesData(FIRQuerySnapshot *docs) { NSMutableArray *> *result = [NSMutableArray array]; for (FIRDocumentChange *docChange in docs.documentChanges) { NSMutableArray *docChangeData = [NSMutableArray array]; [docChangeData addObject:@(docChange.type)]; [docChangeData addObject:docChange.document.documentID]; [docChangeData addObject:docChange.document.data]; [result addObject:docChangeData]; } return result; } extern "C" NSArray *FIRDocumentReferenceArrayFromQuerySnapshot( FIRQuerySnapshot *docs) { NSMutableArray *documentReferenceAccumulator = [[NSMutableArray alloc] init]; for (FIRDocumentSnapshot *documentSnapshot in docs.documents) { [documentReferenceAccumulator addObject:documentSnapshot.reference]; } return [documentReferenceAccumulator copy]; } @end NS_ASSUME_NONNULL_END