aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Example/Tests/Integration/API
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2017-10-03 08:55:22 -0700
committerGravatar GitHub <noreply@github.com>2017-10-03 08:55:22 -0700
commitbde743ed25166a0b320ae157bfb1d68064f531c9 (patch)
tree4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example/Tests/Integration/API
parentbf550507ffa8beee149383a5bf1e2363bccefbb4 (diff)
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0 Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Example/Tests/Integration/API')
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRCursorTests.m195
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m741
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRFieldsTests.m223
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m129
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRQueryTests.m197
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m183
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRTypeTests.m79
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRValidationTests.m560
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m313
9 files changed, 2620 insertions, 0 deletions
diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.m b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m
new file mode 100644
index 0000000..2f8babd
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m
@@ -0,0 +1,195 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRCursorTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRCursorTests
+
+- (void)testCanPageThroughItems {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a"},
+ @"b" : @{@"v" : @"b"},
+ @"c" : @{@"v" : @"c"},
+ @"d" : @{@"v" : @"d"},
+ @"e" : @{@"v" : @"e"},
+ @"f" : @{@"v" : @"f"}
+ }];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[testCollection queryLimitedTo:2]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"v" : @"a"}, @{@"v" : @"b"} ]));
+
+ FIRDocumentSnapshot *lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot),
+ (@[ @{@"v" : @"c"}, @{@"v" : @"d"}, @{@"v" : @"e"} ]));
+
+ lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:1] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[ @{@"v" : @"f"} ]);
+
+ lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[]);
+}
+
+- (void)testCanBeCreatedFromDocuments {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @2.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"c"]];
+
+ XCTAssertTrue(snapshot.exists);
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[query queryStartingAtDocument:snapshot]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"d",
+ @"sort" : @2.0 }
+ ]));
+
+ querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeDocument:snapshot]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"e",
+ @"sort" : @0.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 },
+ @{ @"v" : @"b",
+ @"sort" : @2.0 }
+ ]));
+}
+
+- (void)testCanBeCreatedFromValues {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @2.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"b",
+ @"sort" : @2.0 },
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"d",
+ @"sort" : @2.0 }
+ ]));
+
+ querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"e",
+ @"sort" : @0.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 }
+ ]));
+}
+
+- (void)testCanBeCreatedUsingDocumentId {
+ NSDictionary *testDocs = @{
+ @"a" : @{@"k" : @"a"},
+ @"b" : @{@"k" : @"b"},
+ @"c" : @{@"k" : @"c"},
+ @"d" : @{@"k" : @"d"},
+ @"e" : @{@"k" : @"e"}
+ };
+ FIRCollectionReference *writer = [[[[self firestore] collectionWithPath:@"parent-collection"]
+ documentWithAutoID] collectionWithPath:@"sub-collection"];
+ [self writeAllDocuments:testDocs toCollection:writer];
+
+ FIRCollectionReference *reader = [[self firestore] collectionWithPath:writer.path];
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[[[reader queryOrderedByFieldPath:[FIRFieldPath documentID]]
+ queryStartingAtValues:@[ @"b" ]]
+ queryEndingBeforeValues:@[ @"d" ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot),
+ (@[ @{@"k" : @"b"}, @{@"k" : @"c"} ]));
+}
+
+- (void)testCanBeUsedWithReferenceValues {
+ FIRFirestore *db = [self firestore];
+
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"1a", @"ref" : [db documentWithPath:@"1/a"]},
+ @"b" : @{@"k" : @"1b", @"ref" : [db documentWithPath:@"1/b"]},
+ @"c" : @{@"k" : @"2a", @"ref" : [db documentWithPath:@"2/a"]},
+ @"d" : @{@"k" : @"2b", @"ref" : [db documentWithPath:@"2/b"]},
+ @"e" : @{@"k" : @"3a", @"ref" : [db documentWithPath:@"3/a"]},
+ }];
+ FIRQuery *query = [testCollection queryOrderedByField:@"ref"];
+ FIRQuerySnapshot *querySnapshot = [self
+ readDocumentSetForRef:[[query queryStartingAfterValues:@[ [db documentWithPath:@"1/a"] ]]
+ queryEndingAtValues:@[ [db documentWithPath:@"2/b"] ]]];
+ NSMutableArray<NSString *> *actual = [NSMutableArray array];
+ [querySnapshot.documents enumerateObjectsUsingBlock:^(FIRDocumentSnapshot *_Nonnull doc,
+ NSUInteger idx, BOOL *_Nonnull stop) {
+ [actual addObject:doc.data[@"k"]];
+ }];
+ XCTAssertEqualObjects(actual, (@[ @"1b", @"2a", @"2b" ]));
+}
+
+- (void)testCanBeUsedInDescendingQueries {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @3.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+ FIRQuery *query = [[testCollection queryOrderedByField:@"sort" descending:YES]
+ queryOrderedByFieldPath:[FIRFieldPath documentID]
+ descending:YES];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"b",
+ @"sort" : @2.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 },
+ @{ @"v" : @"e",
+ @"sort" : @0.0 }
+ ]));
+
+ snapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{ @"v" : @"d", @"sort" : @3.0 } ]));
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m
new file mode 100644
index 0000000..d5558cc
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m
@@ -0,0 +1,741 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRDatabaseTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRDatabaseTests
+
+- (void)testCanUpdateAnExistingDocument {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *updateData =
+ @{@"desc" : @"NewDescription", @"owner.email" : @"new@xyz.com"};
+ NSDictionary<NSString *, id> *finalData =
+ @{ @"desc" : @"NewDescription",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"new@xyz.com"} };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *updateCompletion = [self expectationWithDescription:@"updateData"];
+ [doc updateData:updateData
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [updateCompletion fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertTrue(result.exists);
+ XCTAssertEqualObjects(result.data, finalData);
+}
+
+- (void)testCanDeleteAFieldWithAnUpdate {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *updateData =
+ @{@"owner.email" : [FIRFieldValue fieldValueForDelete]};
+ NSDictionary<NSString *, id> *finalData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny"} };
+
+ [self writeDocumentRef:doc data:initialData];
+ [self updateDocumentRef:doc data:updateData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertTrue(result.exists);
+ XCTAssertEqualObjects(result.data, finalData);
+}
+
+- (void)testDeleteDocument {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *data = @{@"value" : @"foo"};
+ [self writeDocumentRef:doc data:data];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, data);
+ [self deleteDocumentRef:doc];
+ result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testCannotUpdateNonexistentDocument {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *setCompletion = [self expectationWithDescription:@"setData"];
+ [doc updateData:@{@"owner" : @"abc"}
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [setCompletion fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testCanOverwriteDataAnExistingDocumentUsingSet {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *udpateData = @{@"desc" : @"NewDescription"};
+
+ [self writeDocumentRef:doc data:initialData];
+ [self writeDocumentRef:doc data:udpateData];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, udpateData);
+}
+
+- (void)testCanMergeDataWithAnExistingDocumentUsingSet {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{
+ @"desc" : @"Description",
+ @"owner.data" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"}
+ };
+ NSDictionary<NSString *, id> *updateData =
+ @{ @"updated" : @YES,
+ @"owner.data" : @{@"name" : @"Sebastian"} };
+ NSDictionary<NSString *, id> *finalData = @{
+ @"desc" : @"Description",
+ @"updated" : @YES,
+ @"owner.data" : @{@"name" : @"Sebastian", @"email" : @"abc@xyz.com"}
+ };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *completed =
+ [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"];
+
+ [doc setData:updateData
+ options:[FIRSetOptions merge]
+ completion:^(NSError *error) {
+ XCTAssertNil(error);
+ [completed fulfill];
+ }];
+
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, finalData);
+}
+
+- (void)testMergeReplacesArrays {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{
+ @"untouched" : @YES,
+ @"data" : @"old",
+ @"topLevel" : @[ @"old", @"old" ],
+ @"mapInArray" : @[ @{@"data" : @"old"} ]
+ };
+ NSDictionary<NSString *, id> *updateData =
+ @{ @"data" : @"new",
+ @"topLevel" : @[ @"new" ],
+ @"mapInArray" : @[ @{@"data" : @"new"} ] };
+ NSDictionary<NSString *, id> *finalData = @{
+ @"untouched" : @YES,
+ @"data" : @"new",
+ @"topLevel" : @[ @"new" ],
+ @"mapInArray" : @[ @{@"data" : @"new"} ]
+ };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *completed =
+ [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"];
+
+ [doc setData:updateData
+ options:[FIRSetOptions merge]
+ completion:^(NSError *error) {
+ XCTAssertNil(error);
+ [completed fulfill];
+ }];
+
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, finalData);
+}
+
+- (void)testAddingToACollectionYieldsTheCorrectDocumentReference {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }];
+
+ XCTestExpectation *getCompletion = [self expectationWithDescription:@"getData"];
+ [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable document,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(document.data, (@{ @"foo" : @1 }));
+
+ [getCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testListenCanBeCalledMultipleTimes {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRDocumentReference *doc = [coll documentWithAutoID];
+
+ XCTestExpectation *completed = [self expectationWithDescription:@"multiple addSnapshotListeners"];
+
+ __block NSDictionary<NSString *, id> *resultingData;
+
+ // Shut the compiler up about strong references to doc.
+ FIRDocumentReference *__weak weakDoc = doc;
+
+ [doc setData:@{@"foo" : @"bar"}
+ completion:^(NSError *error1) {
+ XCTAssertNil(error1);
+ FIRDocumentReference *strongDoc = weakDoc;
+
+ [strongDoc addSnapshotListener:^(FIRDocumentSnapshot *snapshot2, NSError *error2) {
+ XCTAssertNil(error2);
+
+ FIRDocumentReference *strongDoc2 = weakDoc;
+ [strongDoc2 addSnapshotListener:^(FIRDocumentSnapshot *snapshot3, NSError *error3) {
+ XCTAssertNil(error3);
+ resultingData = snapshot3.data;
+ [completed fulfill];
+ }];
+ }];
+ }];
+
+ [self awaitExpectations];
+ XCTAssertEqualObjects(resultingData, @{@"foo" : @"bar"});
+}
+
+- (void)testDocumentSnapshotEvents_nonExistent {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *snapshotCompletion = [self expectationWithDescription:@"snapshot"];
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [snapshotCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forAdd {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *dataCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ [dataCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ dataCompletion = [self expectationWithDescription:@"data snapshot"];
+
+ [docRef setData:@{ @"a" : @1 }];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forAddIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *dataCompletion;
+ __block int callbacks = 0;
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+
+ } else if (callbacks == 3) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ [dataCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ dataCompletion = [self expectationWithDescription:@"data snapshot"];
+
+ [docRef setData:@{ @"a" : @1 }];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forChange {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forChangeIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+
+ } else if (callbacks == 4) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 5) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forDelete {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertFalse(doc.exists);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forDeleteIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTAssertFalse(doc.exists);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forAdd {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *newData = @{ @"a" : @1 };
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 0);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, newData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"changed snapshot"];
+
+ [docRef setData:newData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forChange {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, initialData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, changedData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forDelete {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, initialData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 0);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testExposesFirestoreOnDocumentReferences {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"foo/bar"];
+ XCTAssertEqual(doc.firestore, self.db);
+}
+
+- (void)testExposesFirestoreOnQueries {
+ FIRQuery *q = [[self.db collectionWithPath:@"foo"] queryLimitedTo:5];
+ XCTAssertEqual(q.firestore, self.db);
+}
+
+- (void)testCanTraverseCollectionsAndDocuments {
+ NSString *expected = @"a/b/c/d";
+ // doc path from root Firestore.
+ XCTAssertEqualObjects([self.db documentWithPath:@"a/b/c/d"].path, expected);
+ // collection path from root Firestore.
+ XCTAssertEqualObjects([[self.db collectionWithPath:@"a/b/c"] documentWithPath:@"d"].path,
+ expected);
+ // doc path from CollectionReference.
+ XCTAssertEqualObjects([[self.db collectionWithPath:@"a"] documentWithPath:@"b/c/d"].path,
+ expected);
+ // collection path from DocumentReference.
+ XCTAssertEqualObjects([[self.db documentWithPath:@"a/b"] collectionWithPath:@"c/d/e"].path,
+ @"a/b/c/d/e");
+}
+
+- (void)testCanTraverseCollectionAndDocumentParents {
+ FIRCollectionReference *collection = [self.db collectionWithPath:@"a/b/c"];
+ XCTAssertEqualObjects(collection.path, @"a/b/c");
+
+ FIRDocumentReference *doc = collection.parent;
+ XCTAssertEqualObjects(doc.path, @"a/b");
+
+ collection = doc.parent;
+ XCTAssertEqualObjects(collection.path, @"a");
+
+ FIRDocumentReference *nilDoc = collection.parent;
+ XCTAssertNil(nilDoc);
+}
+
+- (void)testUpdateFieldsWithDots {
+ FIRDocumentReference *doc = [self documentRef];
+
+ [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}];
+
+ [self updateDocumentRef:doc data:@{ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"];
+
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFields {
+ FIRDocumentReference *doc = [self documentRef];
+
+ [self writeDocumentRef:doc
+ data:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }];
+
+ [self updateDocumentRef:doc
+ data:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"];
+
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testCollectionID {
+ XCTAssertEqualObjects([self.db collectionWithPath:@"foo"].collectionID, @"foo");
+ XCTAssertEqualObjects([self.db collectionWithPath:@"foo/bar/baz"].collectionID, @"baz");
+}
+
+- (void)testDocumentID {
+ XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar"].documentID, @"bar");
+ XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar/baz/qux"].documentID, @"qux");
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m
new file mode 100644
index 0000000..d851556
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m
@@ -0,0 +1,223 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRFieldsTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRFieldsTests
+
+- (NSDictionary<NSString *, id> *)testNestedDataNumbered:(int)number {
+ return @{
+ @"name" : [NSString stringWithFormat:@"room %d", number],
+ @"metadata" : @{
+ @"createdAt" : @(number),
+ @"deep" : @{@"field" : [NSString stringWithFormat:@"deep-field-%d", number]}
+ }
+ };
+}
+
+- (void)testNestedFieldsCanBeWrittenWithSet {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, testData);
+}
+
+- (void)testNestedFieldsCanBeReadDirectly {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result[@"name"], testData[@"name"]);
+ XCTAssertEqualObjects(result[@"metadata"], testData[@"metadata"]);
+ XCTAssertEqualObjects(result[@"metadata.deep.field"], testData[@"metadata"][@"deep"][@"field"]);
+ XCTAssertNil(result[@"metadata.nofield"]);
+ XCTAssertNil(result[@"nometadata.nofield"]);
+}
+
+- (void)testNestedFieldsCanBeUpdated {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+ [self updateDocumentRef:doc data:@{ @"metadata.deep.field" : @100, @"metadata.added" : @200 }];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(
+ result.data, (@{
+ @"name" : @"room 1",
+ @"metadata" : @{@"createdAt" : @1, @"deep" : @{@"field" : @100}, @"added" : @200}
+ }));
+}
+
+- (void)testNestedFieldsCanBeUsedInQueryFilters {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testNestedDataNumbered:300],
+ @"2" : [self testNestedDataNumbered:100],
+ @"3" : [self testNestedDataNumbered:200]
+ };
+
+ // inequality adds implicit sort on field
+ NSArray<NSDictionary<NSString *, id> *> *expected =
+ @[ [self testNestedDataNumbered:200], [self testNestedDataNumbered:300] ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *q = [coll queryWhereField:@"metadata.createdAt" isGreaterThanOrEqualTo:@200];
+ FIRQuerySnapshot *results = [self readDocumentSetForRef:q];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (expected));
+}
+
+- (void)testNestedFieldsCanBeUsedInOrderBy {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testNestedDataNumbered:300],
+ @"2" : [self testNestedDataNumbered:100],
+ @"3" : [self testNestedDataNumbered:200]
+ };
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
+ FIRQuery *q = [coll queryOrderedByField:@"metadata.createdAt"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[
+ [self testNestedDataNumbered:100], [self testNestedDataNumbered:200],
+ [self testNestedDataNumbered:300]
+ ]));
+ [queryCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+/**
+ * Creates test data with special characters in field names. Datastore currently prohibits mixing
+ * nested data with special characters so tests that use this data must be separate.
+ */
+- (NSDictionary<NSString *, id> *)testDottedDataNumbered:(int)number {
+ return @{
+ @"a" : [NSString stringWithFormat:@"field %d", number],
+ @"b.dot" : @(number),
+ @"c\\slash" : @(number)
+ };
+}
+
+- (void)testFieldsWithSpecialCharsCanBeWrittenWithSet {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, testData);
+}
+
+- (void)testFieldsWithSpecialCharsCanBeReadDirectly {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result[@"a"], testData[@"a"]);
+ XCTAssertEqualObjects(result[[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]],
+ testData[@"b.dot"]);
+ XCTAssertEqualObjects(result[@"c\\slash"], testData[@"c\\slash"]);
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUpdated {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+ [self updateDocumentRef:doc
+ data:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100,
+ @"c\\slash" : @200
+ }];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, (@{ @"a" : @"field 1", @"b.dot" : @100, @"c\\slash" : @200 }));
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUsedInQueryFilters {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testDottedDataNumbered:300],
+ @"2" : [self testDottedDataNumbered:100],
+ @"3" : [self testDottedDataNumbered:200]
+ };
+
+ // inequality adds implicit sort on field
+ NSArray<NSDictionary<NSString *, id> *> *expected =
+ @[ [self testDottedDataNumbered:200], [self testDottedDataNumbered:300] ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
+ FIRQuery *q = [coll queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]
+ isGreaterThanOrEqualTo:@200];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [queryCompletion fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUsedInOrderBy {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testDottedDataNumbered:300],
+ @"2" : [self testDottedDataNumbered:100],
+ @"3" : [self testDottedDataNumbered:200]
+ };
+
+ NSArray<NSDictionary<NSString *, id> *> *expected = @[
+ [self testDottedDataNumbered:100], [self testDottedDataNumbered:200],
+ [self testDottedDataNumbered:300]
+ ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *q = [coll queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]];
+ XCTestExpectation *queryDot = [self expectationWithDescription:@"query dot"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [queryDot fulfill];
+ }];
+ [self awaitExpectations];
+
+ XCTestExpectation *querySlash = [self expectationWithDescription:@"query slash"];
+ q = [coll queryOrderedByField:@"c\\slash"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [querySlash fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m
new file mode 100644
index 0000000..19771ff
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m
@@ -0,0 +1,129 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRListenerRegistrationTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRListenerRegistrationTests
+
+- (void)testCanBeRemoved {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ __block int callbacks = 0;
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacks++;
+ }];
+
+ id<FIRListenerRegistration> two = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacks++;
+ }];
+
+ // Wait for initial events
+ [self waitUntil:^BOOL {
+ return callbacks == 2;
+ }];
+
+ // Trigger new events
+ [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}];
+
+ // Write events should have triggered
+ XCTAssertEqual(4, callbacks);
+
+ // No more events should occur
+ [one remove];
+ [two remove];
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // Assert no further events occurred
+ XCTAssertEqual(4, callbacks);
+}
+
+- (void)testCanBeRemovedTwice {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+ id<FIRListenerRegistration> two = [docRef
+ addSnapshotListener:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+
+ [one remove];
+ [one remove];
+
+ [two remove];
+ [two remove];
+}
+
+- (void)testCanBeRemovedIndependently {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ __block int callbacksOne = 0;
+ __block int callbacksTwo = 0;
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacksOne++;
+ }];
+
+ id<FIRListenerRegistration> two = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacksTwo++;
+ }];
+
+ // Wait for initial events
+ [self waitUntil:^BOOL {
+ return callbacksOne == 1 && callbacksTwo == 1;
+ }];
+
+ // Trigger new events
+ [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}];
+
+ // Write events should have triggered
+ XCTAssertEqual(2, callbacksOne);
+ XCTAssertEqual(2, callbacksTwo);
+
+ // Should leave "two" unaffected
+ [one remove];
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // Assert only events for "two" actually occurred
+ XCTAssertEqual(2, callbacksOne);
+ XCTAssertEqual(3, callbacksTwo);
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // No more events should occur
+ [two remove];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m
new file mode 100644
index 0000000..f08df33
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m
@@ -0,0 +1,197 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRQueryTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRQueryTests
+
+- (void)testLimitQueries {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"a"},
+ @"b" : @{@"k" : @"b"},
+ @"c" : @{@"k" : @"c"}
+
+ }];
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collRef queryLimitedTo:2]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"k" : @"a"}, @{@"k" : @"b"} ]));
+}
+
+- (void)testLimitQueriesWithDescendingSortOrder {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"a", @"sort" : @0},
+ @"b" : @{@"k" : @"b", @"sort" : @1},
+ @"c" : @{@"k" : @"c", @"sort" : @1},
+ @"d" : @{@"k" : @"d", @"sort" : @2},
+
+ }];
+ FIRQuerySnapshot *snapshot =
+ [self readDocumentSetForRef:[[collRef queryOrderedByField:@"sort" descending:YES]
+ queryLimitedTo:2]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
+ @{ @"k" : @"d",
+ @"sort" : @2 },
+ @{ @"k" : @"c",
+ @"sort" : @1 }
+ ]));
+}
+
+- (void)testKeyOrderIsDescendingForDescendingInequality {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"foo" : @42},
+ @"b" : @{@"foo" : @42.0},
+ @"c" : @{@"foo" : @42},
+ @"d" : @{@"foo" : @21},
+ @"e" : @{@"foo" : @21.0},
+ @"f" : @{@"foo" : @66},
+ @"g" : @{@"foo" : @66.0},
+ }];
+ FIRQuerySnapshot *snapshot =
+ [self readDocumentSetForRef:[[collRef queryWhereField:@"foo" isGreaterThan:@21]
+ queryOrderedByField:@"foo"
+ descending:YES]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"g", @"f", @"c", @"b", @"a" ]));
+}
+
+- (void)testUnaryFilterQueries {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"null" : [NSNull null], @"nan" : @(NAN)},
+ @"b" : @{@"null" : [NSNull null], @"nan" : @0},
+ @"c" : @{@"null" : @NO, @"nan" : @(NAN)}
+ }];
+
+ FIRQuerySnapshot *results =
+ [self readDocumentSetForRef:[[collRef queryWhereField:@"null" isEqualTo:[NSNull null]]
+ queryWhereField:@"nan"
+ isEqualTo:@(NAN)]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[
+ @{ @"null" : [NSNull null],
+ @"nan" : @(NAN) }
+ ]));
+}
+
+- (void)testQueryWithFieldPaths {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"a" : @1},
+ @"b" : @{@"a" : @2},
+ @"c" : @{@"a" : @3}
+ }];
+
+ FIRQuery *query =
+ [collRef queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] isLessThan:@3];
+ query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]]
+ descending:YES];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ]));
+}
+
+- (void)testFilterOnInfinity {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"inf" : @(INFINITY)},
+ @"b" : @{@"inf" : @(-INFINITY)}
+ }];
+
+ FIRQuerySnapshot *results =
+ [self readDocumentSetForRef:[collRef queryWhereField:@"inf" isEqualTo:@(INFINITY)]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ @{ @"inf" : @(INFINITY) } ]));
+}
+
+- (void)testCanExplicitlySortByDocumentID {
+ NSDictionary *testDocs = @{
+ @"a" : @{@"key" : @"a"},
+ @"b" : @{@"key" : @"b"},
+ @"c" : @{@"key" : @"c"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+
+ // Ideally this would be descending to validate it's different than
+ // the default, but that requires an extra index
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryOrderedByFieldPath:[FIRFieldPath documentID]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs),
+ (@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"c"] ]));
+}
+
+- (void)testCanQueryByDocumentID {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:@"ab"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+}
+
+- (void)testCanQueryByDocumentIDs {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:@"ab"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+
+ docs = [self readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isGreaterThan:@"aa"]
+ queryWhereFieldPath:[FIRFieldPath documentID]
+ isLessThanOrEqualTo:@"ba"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ]));
+}
+
+- (void)testCanQueryByDocumentIDsUsingRefs {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs = [self
+ readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:[collection documentWithPath:@"ab"]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+
+ docs = [self
+ readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isGreaterThan:[collection documentWithPath:@"aa"]]
+ queryWhereFieldPath:[FIRFieldPath documentID]
+ isLessThanOrEqualTo:[collection documentWithPath:@"ba"]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ]));
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m
new file mode 100644
index 0000000..1d77e16
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m
@@ -0,0 +1,183 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRServerTimestampTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRServerTimestampTests {
+ // Data written in tests via set.
+ NSDictionary *_setData;
+
+ // Base and update data used for update tests.
+ NSDictionary *_initialData;
+ NSDictionary *_updateData;
+
+ // A document reference to read and write to.
+ FIRDocumentReference *_docRef;
+
+ // Accumulator used to capture events during the test.
+ FSTEventAccumulator *_accumulator;
+
+ // Listener registration for a listener maintained during the course of the test.
+ id<FIRListenerRegistration> _listenerRegistration;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ // Data written in tests via set.
+ _setData = @{
+ @"a" : @42,
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp],
+ @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]}
+ };
+
+ // Base and update data used for update tests.
+ _initialData = @{ @"a" : @42 };
+ _updateData = @{
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp],
+ @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]}
+ };
+
+ _docRef = [self documentRef];
+ _accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ _listenerRegistration = [_docRef addSnapshotListener:_accumulator.handler];
+
+ // Wait for initial nil snapshot to avoid potential races.
+ FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"];
+ XCTAssertFalse(initialSnapshot.exists);
+}
+
+- (void)tearDown {
+ [_listenerRegistration remove];
+
+ [super tearDown];
+}
+
+// Returns the expected data, with an arbitrary timestamp substituted in.
+- (NSDictionary *)expectedDataWithTimestamp:(id _Nullable)timestamp {
+ return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} };
+}
+
+/** Writes _initialData and waits for the corresponding snapshot. */
+- (void)writeInitialData {
+ [self writeDocumentRef:_docRef data:_initialData];
+ FIRDocumentSnapshot *initialDataSnap = [_accumulator awaitEventWithName:@"Initial data event."];
+ XCTAssertEqualObjects(initialDataSnap.data, _initialData);
+}
+
+/** Waits for a snapshot containing _setData but with NSNull for the timestamps. */
+- (void)waitForLocalEvent {
+ FIRDocumentSnapshot *localSnap = [_accumulator awaitEventWithName:@"Local event."];
+ XCTAssertEqualObjects(localSnap.data, [self expectedDataWithTimestamp:[NSNull null]]);
+}
+
+/** Waits for a snapshot containing _setData but with resolved server timestamps. */
+- (void)waitForRemoteEvent {
+ // server event should have a resolved timestamp; verify it.
+ FIRDocumentSnapshot *remoteSnap = [_accumulator awaitEventWithName:@"Remote event"];
+ XCTAssertTrue(remoteSnap.exists);
+ NSDate *when = remoteSnap[@"when"];
+ XCTAssertTrue([when isKindOfClass:[NSDate class]]);
+ // Tolerate up to 10 seconds of clock skew between client and server.
+ XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10);
+
+ // Validate the rest of the document.
+ XCTAssertEqualObjects(remoteSnap.data, [self expectedDataWithTimestamp:when]);
+}
+
+- (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"];
+ [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ transactionBlock(transaction);
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testServerTimestampsWorkViaSet {
+ [self writeDocumentRef:_docRef data:_setData];
+ [self waitForLocalEvent];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaUpdate {
+ [self writeInitialData];
+ [self updateDocumentRef:_docRef data:_updateData];
+ [self waitForLocalEvent];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaTransactionSet {
+ [self runTransactionBlock:^(FIRTransaction *transaction) {
+ [transaction setData:_setData forDocument:_docRef];
+ }];
+
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaTransactionUpdate {
+ [self writeInitialData];
+ [self runTransactionBlock:^(FIRTransaction *transaction) {
+ [transaction updateData:_updateData forDocument:_docRef];
+ }];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsFailViaUpdateOnNonexistentDocument {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"update complete"];
+ [_docRef updateData:_updateData
+ completion:^(NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"];
+ [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ [transaction updateData:_updateData forDocument:_docRef];
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(b/35201829): This should be NotFound, but right now we retry transactions on any
+ // error and so this turns into Aborted instead.
+ // TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want???
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m
new file mode 100644
index 0000000..f3021dd
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m
@@ -0,0 +1,79 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRTypeTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRTypeTests
+
+- (void)assertSuccessfulRoundtrip:(NSDictionary *)data {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+
+ [self writeDocumentRef:doc data:data];
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertTrue(document.exists);
+ XCTAssertEqualObjects(document.data, data);
+}
+
+- (void)testCanReadAndWriteNullFields {
+ [self assertSuccessfulRoundtrip:@{ @"a" : @1, @"b" : [NSNull null] }];
+}
+
+- (void)testCanReadAndWriteArrayFields {
+ [self assertSuccessfulRoundtrip:@{
+ @"array" : @[ @1, @"foo", @{@"deep" : @YES}, [NSNull null] ]
+ }];
+}
+
+- (void)testCanReadAndWriteBlobFields {
+ NSData *data = [NSData dataWithBytes:"\0\1\2" length:3];
+ [self assertSuccessfulRoundtrip:@{@"blob" : data}];
+}
+
+- (void)testCanReadAndWriteGeoPointFields {
+ [self assertSuccessfulRoundtrip:@{
+ @"geoPoint" : [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]
+ }];
+}
+
+- (void)testCanReadAndWriteTimestampFields {
+ // Choose a value that can be converted losslessly between fixed point and double
+ NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:1491847082.125];
+ [self assertSuccessfulRoundtrip:@{@"timestamp" : timestamp}];
+}
+
+- (void)testCanReadAndWriteDocumentReferences {
+ // We can't use assertSuccessfulRoundtrip since FIRDocumentReference doesn't implement isEqual.
+ FIRDocumentReference *docRef = [self.db documentWithPath:@"rooms/eros"];
+ id data = @{ @"a" : @42, @"ref" : docRef };
+ [self writeDocumentRef:docRef data:data];
+
+ FIRDocumentSnapshot *readDoc = [self readDocumentForRef:docRef];
+ XCTAssertTrue(readDoc.exists);
+
+ XCTAssertEqualObjects(readDoc[@"a"], data[@"a"]);
+ FIRDocumentReference *readDocRef = readDoc[@"ref"];
+ XCTAssertTrue([readDocRef isKindOfClass:[FIRDocumentReference class]]);
+ XCTAssertEqualObjects(readDocRef.path, docRef.path);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m
new file mode 100644
index 0000000..1ba1d7a
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m
@@ -0,0 +1,560 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTHelpers.h"
+#import "FSTIntegrationTestCase.h"
+
+// We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings.
+#pragma clang diagnostic ignored "-Wnonnull"
+
+@interface FIRValidationTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRValidationTests
+
+#pragma mark - FIRFirestoreSettings Validation
+
+- (void)testNilHostFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ FSTAssertThrows(settings.host = nil,
+ @"host setting may not be nil. You should generally just use the default value "
+ "(which is firestore.googleapis.com)");
+}
+
+- (void)testNilDispatchQueueFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ FSTAssertThrows(settings.dispatchQueue = nil,
+ @"dispatch queue setting may not be nil. Create a new dispatch queue with "
+ "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default "
+ "(which is the main queue, returned from dispatch_get_main_queue())");
+}
+
+- (void)testChangingSettingsAfterUseFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ [[self.db documentWithPath:@"foo/bar"] setData:@{ @"a" : @42 }];
+ settings.host = @"example.com";
+ FSTAssertThrows(self.db.settings = settings,
+ @"Firestore instance has already been started and its settings can no longer be "
+ @"changed. You can only set settings before calling any other methods on "
+ @"a Firestore instance.");
+}
+
+#pragma mark - FIRFirestore Validation
+
+- (void)testNilFIRAppFails {
+ FSTAssertThrows(
+ [FIRFirestore firestoreForApp:nil],
+ @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd like to use the "
+ "default FirebaseApp instance.");
+}
+
+// TODO(b/62410906): Test for firestoreForApp:database: with nil DatabaseID.
+
+- (void)testNilTransactionBlocksFail {
+ FSTAssertThrows([self.db runTransactionWithBlock:nil
+ completion:^(id result, NSError *error) {
+ XCTFail(@"Completion shouldn't run.");
+ }],
+ @"Transaction block cannot be nil.");
+
+ FSTAssertThrows(
+ [self.db runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ XCTFail(@"Transaction block shouldn't run.");
+ return nil;
+ }
+ completion:nil],
+ @"Transaction completion block cannot be nil.");
+}
+
+#pragma mark - Collection and Document Path Validation
+
+- (void)testNilCollectionPathsFail {
+ FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"];
+ NSString *nilError = @"Collection path cannot be nil.";
+ FSTAssertThrows([self.db collectionWithPath:nil], nilError);
+ FSTAssertThrows([baseDocRef collectionWithPath:nil], nilError);
+}
+
+- (void)testWrongLengthCollectionPathsFail {
+ FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"];
+ NSArray *badAbsolutePaths = @[ @"foo/bar", @"foo/bar/baz/quu" ];
+ NSArray *badRelativePaths = @[ @"", @"baz/quu" ];
+ NSArray *badPathLengths = @[ @2, @4 ];
+ NSString *errorFormat =
+ @"Invalid collection reference. Collection references must have an odd "
+ @"number of segments, but %@ has %@";
+ for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) {
+ NSString *error =
+ [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]];
+ FSTAssertThrows([self.db collectionWithPath:badAbsolutePaths[i]], error);
+ FSTAssertThrows([baseDocRef collectionWithPath:badRelativePaths[i]], error);
+ }
+}
+
+- (void)testNilDocumentPathsFail {
+ FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"];
+ NSString *nilError = @"Document path cannot be nil.";
+ FSTAssertThrows([self.db documentWithPath:nil], nilError);
+ FSTAssertThrows([baseCollectionRef documentWithPath:nil], nilError);
+}
+
+- (void)testWrongLengthDocumentPathsFail {
+ FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"];
+ NSArray *badAbsolutePaths = @[ @"foo", @"foo/bar/baz" ];
+ NSArray *badRelativePaths = @[ @"", @"bar/baz" ];
+ NSArray *badPathLengths = @[ @1, @3 ];
+ NSString *errorFormat =
+ @"Invalid document reference. Document references must have an even "
+ @"number of segments, but %@ has %@";
+ for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) {
+ NSString *error =
+ [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]];
+ FSTAssertThrows([self.db documentWithPath:badAbsolutePaths[i]], error);
+ FSTAssertThrows([baseCollectionRef documentWithPath:badRelativePaths[i]], error);
+ }
+}
+
+- (void)testPathsWithEmptySegmentsFail {
+ // We're only testing using collectionWithPath since the validation happens in FSTPath which is
+ // shared by all methods that accept paths.
+
+ // leading / trailing slashes are okay.
+ [self.db collectionWithPath:@"/foo/"];
+ [self.db collectionWithPath:@"/foo"];
+ [self.db collectionWithPath:@"foo/"];
+
+ FSTAssertThrows([self.db collectionWithPath:@"foo//bar/baz"],
+ @"Invalid path (foo//bar/baz). Paths must not contain // in them.");
+ FSTAssertThrows([self.db collectionWithPath:@"//foo"],
+ @"Invalid path (//foo). Paths must not contain // in them.");
+ FSTAssertThrows([self.db collectionWithPath:@"foo//"],
+ @"Invalid path (foo//). Paths must not contain // in them.");
+}
+
+#pragma mark - Write Validation
+
+- (void)testWritesWithNonDictionaryValuesFail {
+ NSArray *badData = @[
+ @42, @"test", @[ @1 ], [NSDate date], [NSNull null], [FIRFieldValue fieldValueForDelete],
+ [FIRFieldValue fieldValueForServerTimestamp]
+ ];
+
+ for (id data in badData) {
+ [self expectWrite:data toFailWithReason:@"Data to be written must be an NSDictionary."];
+ }
+}
+
+- (void)testWritesWithNestedArraysFail {
+ [self expectWrite:@{
+ @"nested-array" : @[ @1, @[ @2 ] ]
+ }
+ toFailWithReason:@"Nested arrays are not supported"];
+}
+
+- (void)testWritesWithInvalidTypesFail {
+ [self expectWrite:@{
+ @"foo" : @{@"bar" : self}
+ }
+ toFailWithReason:@"Unsupported type: FIRValidationTests (found in field foo.bar)"];
+}
+
+- (void)testWritesWithLargeNumbersFail {
+ NSNumber *num = @((unsigned long long)LONG_MAX + 1);
+ NSString *reason =
+ [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num];
+ [self expectWrite:@{@"num" : num} toFailWithReason:reason];
+}
+
+- (void)testWritesWithReferencesToADifferentDatabaseFail {
+ FIRDocumentReference *ref =
+ [[self firestoreWithProjectID:@"different-db"] documentWithPath:@"baz/quu"];
+ id data = @{@"foo" : ref};
+ [self expectWrite:data
+ toFailWithReason:
+ [NSString
+ stringWithFormat:@"Document Reference is for database different-db/(default) but "
+ "should be for database %@/(default) (found in field foo)",
+ [FSTIntegrationTestCase projectID]]];
+}
+
+- (void)testWritesWithReservedFieldsFail {
+ [self expectWrite:@{
+ @"__baz__" : @1
+ }
+ toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"];
+ [self expectWrite:@{
+ @"foo" : @{@"__baz__" : @1}
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field foo.__baz__)"];
+ [self expectWrite:@{
+ @"__baz__" : @{@"foo" : @1}
+ }
+ toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"];
+
+ [self expectUpdate:@{
+ @"foo.__baz__" : @1
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field foo.__baz__)"];
+ [self expectUpdate:@{
+ @"__baz__.foo" : @1
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field __baz__.foo)"];
+ [self expectUpdate:@{
+ @1 : @1
+ }
+ toFailWithReason:@"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."];
+}
+
+- (void)testSetsWithFieldValueDeleteFail {
+ [self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]}
+ toFailWithReason:@"FieldValue.delete() can only be used with updateData()."];
+}
+
+- (void)testUpdatesWithNestedFieldValueDeleteFail {
+ [self expectUpdate:@{
+ @"foo" : @{@"bar" : [FIRFieldValue fieldValueForDelete]}
+ }
+ toFailWithReason:
+ @"FieldValue.delete() can only appear at the top level of your update data "
+ "(found in field foo.bar)"];
+}
+
+- (void)testBatchWritesWithIncorrectReferencesFail {
+ FIRFirestore *db1 = [self firestore];
+ FIRFirestore *db2 = [self firestore];
+ XCTAssertNotEqual(db1, db2);
+
+ NSString *reason = @"Provided document reference is from a different Firestore instance.";
+ id data = @{ @"foo" : @1 };
+ FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"];
+ FIRWriteBatch *batch = [db1 batch];
+ FSTAssertThrows([batch setData:data forDocument:badRef], reason);
+ FSTAssertThrows([batch setData:data forDocument:badRef options:[FIRSetOptions merge]], reason);
+ FSTAssertThrows([batch updateData:data forDocument:badRef], reason);
+ FSTAssertThrows([batch deleteDocument:badRef], reason);
+}
+
+- (void)testTransactionWritesWithIncorrectReferencesFail {
+ FIRFirestore *db1 = [self firestore];
+ FIRFirestore *db2 = [self firestore];
+ XCTAssertNotEqual(db1, db2);
+
+ NSString *reason = @"Provided document reference is from a different Firestore instance.";
+ id data = @{ @"foo" : @1 };
+ FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"];
+
+ XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"];
+ [db1 runTransactionWithBlock:^id(FIRTransaction *txn, NSError **pError) {
+ FSTAssertThrows([txn getDocument:badRef error:nil], reason);
+ FSTAssertThrows([txn setData:data forDocument:badRef], reason);
+ FSTAssertThrows([txn setData:data forDocument:badRef options:[FIRSetOptions merge]], reason);
+ FSTAssertThrows([txn updateData:data forDocument:badRef], reason);
+ FSTAssertThrows([txn deleteDocument:badRef], reason);
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ // ends up being a no-op transaction.
+ XCTAssertNil(error);
+ [transactionDone fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+#pragma mark - Field Path validation
+// TODO(b/37244157): More validation for invalid field paths.
+
+- (void)testFieldPathsWithEmptySegmentsFail {
+ NSArray *badFieldPaths = @[ @"", @"foo..baz", @".foo", @"foo." ];
+
+ for (NSString *fieldPath in badFieldPaths) {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"Invalid field path (%@). Paths must not be empty, begin with "
+ @"'.', end with '.', or contain '..'",
+ fieldPath];
+ [self expectFieldPath:fieldPath toFailWithReason:reason];
+ }
+}
+
+- (void)testFieldPathsWithInvalidSegmentsFail {
+ NSArray *badFieldPaths = @[ @"foo~bar", @"foo*bar", @"foo/bar", @"foo[1", @"foo]1", @"foo[1]" ];
+
+ for (NSString *fieldPath in badFieldPaths) {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'",
+ fieldPath];
+ [self expectFieldPath:fieldPath toFailWithReason:reason];
+ }
+}
+
+#pragma mark - Query Validation
+
+- (void)testQueryWithNonPositiveLimitFails {
+ FSTAssertThrows([[self collectionRef] queryLimitedTo:0],
+ @"Invalid Query. Query limit (0) is invalid. Limit must be positive.");
+ FSTAssertThrows([[self collectionRef] queryLimitedTo:-1],
+ @"Invalid Query. Query limit (-1) is invalid. Limit must be positive.");
+}
+
+- (void)testQueryInequalityOnNullOrNaNFails {
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil],
+ @"Invalid Query. You can only perform equality comparisons on nil / NSNull.");
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]],
+ @"Invalid Query. You can only perform equality comparisons on nil / NSNull.");
+
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)],
+ @"Invalid Query. You can only perform equality comparisons on NaN.");
+}
+
+- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0}
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"f"]];
+ XCTAssertTrue(snapshot.exists);
+
+ NSString *reason =
+ @"Invalid query. You are trying to start or end a query using a document for "
+ "which the field 'sort' (used as the order by) does not exist.";
+ FSTAssertThrows([query queryStartingAtDocument:snapshot], reason);
+ FSTAssertThrows([query queryStartingAfterDocument:snapshot], reason);
+ FSTAssertThrows([query queryEndingBeforeDocument:snapshot], reason);
+ FSTAssertThrows([query queryEndingAtDocument:snapshot], reason);
+}
+
+- (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByField:@"foo"];
+
+ NSString *reason =
+ @"Invalid query. You are trying to start or end a query using more values "
+ "than were specified in the order by.";
+ // More elements than order by
+ FSTAssertThrows(([query queryStartingAtValues:@[ @1, @2 ]]), reason);
+ FSTAssertThrows(([[query queryOrderedByField:@"bar"] queryStartingAtValues:@[ @1, @2, @3 ]]),
+ reason);
+}
+
+- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]];
+ FSTAssertThrows([query queryStartingAtValues:@[ @1 ]],
+ @"Invalid query. Expected a string for the document ID.");
+ FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]],
+ @"Invalid query. Document ID 'foo/bar' contains a slash.");
+}
+
+- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByField:@"foo"];
+ NSString *reason =
+ @"Invalid query. You must not specify a starting point before specifying the order by.";
+ FSTAssertThrows([[query queryStartingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ FSTAssertThrows([[query queryStartingAfterValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ reason = @"Invalid query. You must not specify an ending point before specifying the order by.";
+ FSTAssertThrows([[query queryEndingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+}
+
+- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences {
+ FIRCollectionReference *collection = [self collectionRef];
+ NSString *reason =
+ @"Invalid query. When querying by document ID you must provide a valid "
+ "document ID, but it was an empty string.";
+ FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason);
+
+ reason =
+ @"Invalid query. When querying by document ID you must provide a valid document ID, "
+ "but 'foo/bar/baz' contains a '/' character.";
+ FSTAssertThrows(
+ [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason);
+
+ reason =
+ @"Invalid query. When querying by document ID you must provide a valid string or "
+ "DocumentReference, but it was of type: __NSCFNumber";
+ FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason);
+}
+
+- (void)testQueryInequalityFieldMustMatchFirstOrderByField {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32];
+
+ FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"],
+ @"Invalid Query. All where filters with an inequality (lessThan, "
+ "lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same "
+ "field. But you have inequality filters on 'x' and 'y'");
+
+ NSString *reason =
+ @"Invalid query. You have a where filter with "
+ "an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) "
+ "on field 'x' and so you must also use 'x' as your first queryOrderedBy field, "
+ "but your first queryOrderedBy is currently on field 'y' instead.";
+ FSTAssertThrows([base queryOrderedByField:@"y"], reason);
+ FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isGreaterThan:@32], reason);
+ FSTAssertThrows([[base queryOrderedByField:@"y"] queryOrderedByField:@"x"], reason);
+ FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x"
+ isGreaterThan:@32],
+ reason);
+
+ XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"],
+ @"Same inequality fields work");
+
+ XCTAssertNoThrow([base queryWhereField:@"y" isEqualTo:@"cat"],
+ @"Inequality and equality on different fields works");
+
+ XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works");
+ XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32],
+ @"inequality same as order by works");
+ XCTAssertNoThrow([[base queryOrderedByField:@"x"] queryOrderedByField:@"y"],
+ @"inequality same as first order by works.");
+ XCTAssertNoThrow([[[coll queryOrderedByField:@"x"] queryOrderedByField:@"y"] queryWhereField:@"x"
+ isGreaterThan:@32],
+ @"inequality same as first order by works.");
+}
+
+#pragma mark - GeoPoint Validation
+
+- (void)testInvalidGeoPointParameters {
+ [self verifyExceptionForInvalidLatitude:NAN];
+ [self verifyExceptionForInvalidLatitude:-INFINITY];
+ [self verifyExceptionForInvalidLatitude:INFINITY];
+ [self verifyExceptionForInvalidLatitude:-90.1];
+ [self verifyExceptionForInvalidLatitude:90.1];
+
+ [self verifyExceptionForInvalidLongitude:NAN];
+ [self verifyExceptionForInvalidLongitude:-INFINITY];
+ [self verifyExceptionForInvalidLongitude:INFINITY];
+ [self verifyExceptionForInvalidLongitude:-180.1];
+ [self verifyExceptionForInvalidLongitude:180.1];
+}
+
+#pragma mark - Helpers
+
+/** Performs a write using each write API and makes sure it fails with the expected reason. */
+- (void)expectWrite:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:YES];
+}
+
+/** Performs a write using each set API and makes sure it fails with the expected reason. */
+- (void)expectSet:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:NO];
+}
+
+/** Performs a write using each update API and makes sure it fails with the expected reason. */
+- (void)expectUpdate:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:NO includeUpdates:YES];
+}
+
+/**
+ * Performs a write using each set and/or update API and makes sure it fails with the expected
+ * reason.
+ */
+- (void)expectWrite:(id)data
+ toFailWithReason:(NSString *)reason
+ includeSets:(BOOL)includeSets
+ includeUpdates:(BOOL)includeUpdates {
+ FIRDocumentReference *ref = [self documentRef];
+ if (includeSets) {
+ FSTAssertThrows([ref setData:data], reason, @"for %@", data);
+ FSTAssertThrows([[ref.firestore batch] setData:data forDocument:ref], reason, @"for %@", data);
+ }
+
+ if (includeUpdates) {
+ FSTAssertThrows([ref updateData:data], reason, @"for %@", data);
+ FSTAssertThrows([[ref.firestore batch] updateData:data forDocument:ref], reason, @"for %@",
+ data);
+ }
+
+ XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"];
+ [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ if (includeSets) {
+ FSTAssertThrows([transaction setData:data forDocument:ref], reason, @"for %@", data);
+ }
+ if (includeUpdates) {
+ FSTAssertThrows([transaction updateData:data forDocument:ref], reason, @"for %@", data);
+ }
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ // ends up being a no-op transaction.
+ XCTAssertNil(error);
+ [transactionDone fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testFieldNamesMustNotBeEmpty {
+ NSString *reason = @"Invalid field path. Provided names must not be empty.";
+ FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[]], reason);
+
+ reason = @"Invalid field name at index 0. Field names must not be empty.";
+ FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[ @"" ]], reason);
+
+ reason = @"Invalid field name at index 1. Field names must not be empty.";
+ FSTAssertThrows(([[FIRFieldPath alloc] initWithFields:@[ @"foo", @"" ]]), reason);
+}
+
+/**
+ * Tests a field path with all of our APIs that accept field paths and ensures they fail with the
+ * specified reason.
+ */
+- (void)expectFieldPath:(NSString *)fieldPath toFailWithReason:(NSString *)reason {
+ // Get an arbitrary snapshot we can use for testing.
+ FIRDocumentReference *docRef = [self documentRef];
+ [self writeDocumentRef:docRef data:@{ @"test" : @1 }];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:docRef];
+
+ // Update paths.
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ dict[fieldPath] = @1;
+ [self expectUpdate:dict toFailWithReason:reason];
+
+ // Snapshot fields.
+ FSTAssertThrows(snapshot[fieldPath], reason);
+
+ // Query filter / order fields.
+ FIRCollectionReference *collection = [self collectionRef];
+ FSTAssertThrows([collection queryWhereField:fieldPath isEqualTo:@1], reason);
+ // isLessThan, etc. omitted for brevity since the code path is trivially shared.
+ FSTAssertThrows([collection queryOrderedByField:fieldPath], reason);
+}
+
+- (void)verifyExceptionForInvalidLatitude:(double)latitude {
+ NSString *reason = [NSString
+ stringWithFormat:@"GeoPoint requires a latitude value in the range of [-90, 90], but was %f",
+ latitude];
+ FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:latitude longitude:0], reason);
+}
+
+- (void)verifyExceptionForInvalidLongitude:(double)longitude {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"GeoPoint requires a longitude value in the range of [-180, 180], but was %f",
+ longitude];
+ FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:0 longitude:longitude], reason);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m
new file mode 100644
index 0000000..159cbd7
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m
@@ -0,0 +1,313 @@
+/*
+ * 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 Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRWriteBatchTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRWriteBatchTests
+
+- (void)testSupportEmptyBatches {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ [[[self firestore] batch] commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testSetDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{@"a" : @"b"} forDocument:doc];
+ [batch setData:@{@"c" : @"d"} forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"});
+}
+
+- (void)testSetDocumentWithMerge {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc];
+ [batch setData:@{
+ @"c" : @"d",
+ @"nested" : @{@"c" : @"d"}
+ }
+ forDocument:doc
+ options:[FIRSetOptions merge]];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(
+ snapshot.data, (
+ @{ @"a" : @"b",
+ @"c" : @"d",
+ @"nested" : @{@"a" : @"b", @"c" : @"d"} }));
+}
+
+- (void)testUpdateDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch updateData:@{ @"baz" : @42 } forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(snapshot.data, (@{ @"foo" : @"bar", @"baz" : @42 }));
+}
+
+- (void)testCannotUpdateNonexistentDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch updateData:@{ @"baz" : @42 } forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNotNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testDeleteDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+
+ XCTAssertTrue(snapshot.exists);
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch deleteDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ snapshot = [self readDocumentForRef:doc];
+ XCTAssertFalse(snapshot.exists);
+}
+
+- (void)testBatchesCommitAtomicallyRaisingCorrectEvents {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write two documents.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{ @"a" : @1 } forDocument:docA];
+ [batch setData:@{ @"b" : @2 } forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ]));
+
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ]));
+}
+
+- (void)testBatchesFailAtomicallyRaisingCorrectEvents {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write 1 document and update a nonexistent document.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch failed"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{ @"a" : @1 } forDocument:docA];
+ [batch updateData:@{ @"b" : @2 } forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [expectation fulfill];
+ }];
+
+ // Local event with the set document.
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 } ]));
+
+ // Server event with the set reverted.
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqual(serverSnap.count, 0);
+}
+
+- (void)testWriteTheSameServerTimestampAcrossWrites {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write 2 documents with server timestamps.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docA];
+ [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap),
+ (@[ @{@"when" : [NSNull null]}, @{@"when" : [NSNull null]} ]));
+
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqual(serverSnap.count, 2);
+ NSDate *when = serverSnap.documents[0][@"when"];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap),
+ (@[ @{@"when" : when}, @{@"when" : when} ]));
+}
+
+- (void)testCanWriteTheSameDocumentMultipleTimes {
+ FIRDocumentReference *doc = [self documentRef];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [doc
+ addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertFalse(initialSnap.exists);
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch deleteDocument:doc];
+ [batch setData:@{ @"a" : @1, @"b" : @1, @"when" : @"when" } forDocument:doc];
+ [batch updateData:@{
+ @"b" : @2,
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp]
+ }
+ forDocument:doc];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(localSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : [NSNull null] }));
+
+ FIRDocumentSnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ NSDate *when = serverSnap[@"when"];
+ XCTAssertEqualObjects(serverSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : when }));
+}
+
+- (void)testUpdateFieldsWithDots {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc];
+ [batch updateData:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"
+ }
+ forDocument:doc];
+
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ }];
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFields {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }
+ forDocument:doc];
+ [batch updateData:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }
+ forDocument:doc];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ }];
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+@end