diff options
author | Gil <mcg@google.com> | 2017-10-03 08:55:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-03 08:55:22 -0700 |
commit | bde743ed25166a0b320ae157bfb1d68064f531c9 (patch) | |
tree | 4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example/Tests/Integration/API | |
parent | bf550507ffa8beee149383a5bf1e2363bccefbb4 (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')
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 |