mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
Remove support for running against Hexa, which we haven't done in years. Integration tests will now assume a local emulator by default. Once imported into google3, this will make it significantly easier to run integration tests against cls from third-parties because this will be able to use the emulator we're already running rather than requiring us to build hexa again.
547 lines
20 KiB
Plaintext
547 lines
20 KiB
Plaintext
/*
|
|
* 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 <FirebaseFirestore/FIRCollectionReference.h>
|
|
#import <FirebaseFirestore/FIRDocumentChange.h>
|
|
#import <FirebaseFirestore/FIRDocumentReference.h>
|
|
#import <FirebaseFirestore/FIRDocumentSnapshot.h>
|
|
#import <FirebaseFirestore/FIRFirestore.h>
|
|
#import <FirebaseFirestore/FIRFirestoreSettings.h>
|
|
#import <FirebaseFirestore/FIRQuerySnapshot.h>
|
|
#import <FirebaseFirestore/FIRSnapshotMetadata.h>
|
|
#import <FirebaseFirestore/FIRTransaction.h>
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
|
|
#import "Firestore/Example/Tests/Util/FIRFirestore+Testing.h"
|
|
#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h"
|
|
#import "Firestore/Source/API/FIRFirestore+Internal.h"
|
|
|
|
#include "Firestore/core/src/auth/credentials_provider.h"
|
|
#include "Firestore/core/src/auth/empty_credentials_provider.h"
|
|
#include "Firestore/core/src/auth/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"
|
|
|
|
namespace testutil = firebase::firestore::testutil;
|
|
namespace util = firebase::firestore::util;
|
|
|
|
using firebase::firestore::auth::CredentialChangeListener;
|
|
using firebase::firestore::auth::CredentialsProvider;
|
|
using firebase::firestore::auth::EmptyCredentialsProvider;
|
|
using firebase::firestore::auth::User;
|
|
using firebase::firestore::core::DatabaseInfo;
|
|
using firebase::firestore::local::LevelDbOpener;
|
|
using firebase::firestore::model::DatabaseId;
|
|
using firebase::firestore::remote::GrpcConnection;
|
|
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::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 FakeCredentialsProvider : public EmptyCredentialsProvider {
|
|
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::auth::User::FromUid(new_id));
|
|
}
|
|
}
|
|
|
|
private:
|
|
CredentialChangeListener listener_;
|
|
};
|
|
|
|
@implementation FSTIntegrationTestCase {
|
|
NSMutableArray<FIRFirestore *> *_firestores;
|
|
std::shared_ptr<FakeCredentialsProvider> _fakeCredentialsProvider;
|
|
}
|
|
|
|
- (void)setUp {
|
|
[super setUp];
|
|
|
|
LoadXCTestCaseAwait();
|
|
|
|
_fakeCredentialsProvider = std::make_shared<FakeCredentialsProvider>();
|
|
|
|
[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<Path> 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];
|
|
defaultSettings.persistenceEnabled = YES;
|
|
|
|
// Check for a MobileHarness configuration, running against nightly or prod, which have live
|
|
// SSL certs.
|
|
NSString *project = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"];
|
|
NSString *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.
|
|
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(util::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 = util::MakeString(app.options.projectID);
|
|
FIRFirestore *firestore =
|
|
[[FIRFirestore alloc] initWithDatabaseID:DatabaseId(projectID)
|
|
persistenceKey:util::MakeString(persistenceKey)
|
|
credentialsProvider:_fakeCredentialsProvider
|
|
workerQueue:AsyncQueueForTesting()
|
|
firebaseMetadataProvider:absl::make_unique<FirebaseMetadataProviderApple>(app)
|
|
firebaseApp:app
|
|
instanceRegistry:nil];
|
|
|
|
firestore.settings = [FSTIntegrationTestCase settings];
|
|
[_firestores addObject:firestore];
|
|
return firestore;
|
|
}
|
|
|
|
- (void)triggerUserChangeWithUid:(NSString *)uid {
|
|
_fakeCredentialsProvider->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<FIRListenerRegistration> 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<NSString *, NSDictionary<NSString *, id> *> *)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<NSString *, NSDictionary<NSString *, id> *> *)documents
|
|
toCollection:(FIRCollectionReference *)collection {
|
|
[documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary<NSString *, id> *value,
|
|
BOOL *) {
|
|
FIRDocumentReference *ref = [collection documentWithPath:key];
|
|
[self writeDocumentRef:ref data:value];
|
|
}];
|
|
}
|
|
|
|
- (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 {
|
|
__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<FIRListenerRegistration> 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;
|
|
}
|
|
|
|
- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data {
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@"setData"];
|
|
[ref setData:data completion:[self completionForExpectation:expectation]];
|
|
[self awaitExpectation:expectation];
|
|
}
|
|
|
|
- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<id, id> *)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<NSString *, id> *)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<NSString *, id> *)data {
|
|
XCTestExpectation *expectation = [self expectationWithDescription:@"setDataWithMerge"];
|
|
[ref setData:data merge:YES completion:[self completionForExpectation:expectation]];
|
|
[self awaitExpectation:expectation];
|
|
}
|
|
|
|
- (void)mergeDocumentRef:(FIRDocumentReference *)ref
|
|
data:(NSDictionary<NSString *, id> *)data
|
|
fields:(NSArray<id> *)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<util::AsyncQueue> &)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<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(
|
|
FIRQuerySnapshot *docs) {
|
|
NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
|
|
for (FIRDocumentSnapshot *doc in docs.documents) {
|
|
[result addObject:doc.data];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
extern "C" NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) {
|
|
NSMutableArray<NSString *> *result = [NSMutableArray array];
|
|
for (FIRDocumentSnapshot *doc in docs.documents) {
|
|
[result addObject:doc.documentID];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
extern "C" NSArray<NSArray<id> *> *FIRQuerySnapshotGetDocChangesData(FIRQuerySnapshot *docs) {
|
|
NSMutableArray<NSMutableArray<id> *> *result = [NSMutableArray array];
|
|
for (FIRDocumentChange *docChange in docs.documentChanges) {
|
|
NSMutableArray<id> *docChangeData = [NSMutableArray array];
|
|
[docChangeData addObject:@(docChange.type)];
|
|
[docChangeData addObject:docChange.document.documentID];
|
|
[docChangeData addObject:docChange.document.data];
|
|
[result addObject:docChangeData];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|