| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- /*
- * 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 <FirebaseFirestore/FirebaseFirestore.h>
- #import <XCTest/XCTest.h>
- #include <libkern/OSAtomic.h>
- #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
- @interface FSTTransactionTests : FSTIntegrationTestCase
- @end
- @implementation FSTTransactionTests
- - (void)testGetDocuments {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"spaces"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"foo" : @1, @"desc" : @"Stuff", @"owner" : @"Jonny"}];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertNil(result);
- // We currently require every document read to also be written.
- // TODO(b/34879758): Fix this check once we drop that requirement.
- XCTAssertNotNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- }
- - (void)testDeleteDocument {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertEqualObjects(@"bar", snapshot[@"foo"]);
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- [transaction deleteDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertEqualObjects(@YES, result);
- XCTAssertNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- snapshot = [self readDocumentForRef:doc];
- XCTAssertFalse(snapshot.exists);
- }
- - (void)testGetNonexistentDocumentThenCreate {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- XCTAssertNotNil(snapshot);
- XCTAssertFalse(snapshot.exists);
- [transaction setData:@{@"foo" : @"bar"} forDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertEqualObjects(@YES, result);
- XCTAssertNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertTrue(snapshot.exists);
- XCTAssertEqualObjects(@"bar", snapshot[@"foo"]);
- }
- - (void)testGetNonexistentDocumentThenFailPatch {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- XCTAssertFalse(snapshot.exists);
- [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertNil(result);
- XCTAssertNotNil(error);
- XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
- // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
- // should update the code once the underlying error was fixed.
- XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- }
- - (void)testDeleteDocumentAndPatch {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- XCTAssertTrue(snapshot.exists);
- [transaction deleteDocument:doc];
- // Since we deleted the doc, the update will fail
- [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertNil(result);
- XCTAssertNotNil(error);
- XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
- // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
- // should update the code once the underlying error was fixed.
- XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- }
- - (void)testDeleteDocumentAndSet {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- XCTAssertTrue(snapshot.exists);
- [transaction deleteDocument:doc];
- // TODO(dimond): In theory this should work, but it's complex to make it work, so instead we
- // just let the transaction fail and verify it's unsupported for now
- [transaction setData:@{@"foo" : @"new-bar"} forDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertNil(result);
- XCTAssertNotNil(error);
- XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
- // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
- // should update the code once the underlying error was fixed.
- XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- }
- - (void)testWriteDocumentTwice {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
- [transaction setData:@{@"a" : @"b"} forDocument:doc];
- [transaction setData:@{@"c" : @"d"} forDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertEqualObjects(@YES, result);
- XCTAssertNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"});
- }
- - (void)testSetDocumentWithMerge {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- [transaction setData:@{@"a" : @"b", @"nested" : @{@"a" : @"b"}} forDocument:doc];
- [transaction setData:@{@"c" : @"d", @"nested" : @{@"c" : @"d"}} forDocument:doc merge:YES];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertEqualObjects(@YES, result);
- XCTAssertNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertEqualObjects(snapshot.data,
- (@{@"a" : @"b", @"c" : @"d", @"nested" : @{@"a" : @"b", @"c" : @"d"}}));
- }
- - (void)testCannotUpdateNonExistentDocument {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
- return nil;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertNotNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
- XCTAssertFalse(result.exists);
- }
- - (void)testIncrementTransactionally {
- // A barrier to make sure every transaction reaches the same spot.
- dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0);
- __block volatile int32_t started = 0;
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"count" : @(5.0)}];
- // Make 3 transactions that will all increment.
- int total = 3;
- for (int i = 0; i < total; i++) {
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- int32_t nowStarted = OSAtomicIncrement32(&started);
- // Once all of the transactions have read, allow the first write.
- if (nowStarted == total) {
- dispatch_semaphore_signal(writeBarrier);
- }
- dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER);
- // Refill the barrier so that the other transactions and retries succeed.
- dispatch_semaphore_signal(writeBarrier);
- double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0;
- [transaction setData:@{@"count" : @(newCount)} forDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- [expectation fulfill];
- }];
- }
- [self awaitExpectations];
- // Now all transaction should be completed, so check the result.
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]);
- }
- - (void)testUpdateTransactionally {
- // A barrier to make sure every transaction reaches the same spot.
- dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0);
- __block volatile int32_t started = 0;
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"count" : @(5.0), @"other" : @"yes"}];
- // Make 3 transactions that will all increment.
- int total = 3;
- for (int i = 0; i < total; i++) {
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- int32_t nowStarted = OSAtomicIncrement32(&started);
- // Once all of the transactions have read, allow the first write.
- if (nowStarted == total) {
- dispatch_semaphore_signal(writeBarrier);
- }
- dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER);
- // Refill the barrier so that the other transactions and retries succeed.
- dispatch_semaphore_signal(writeBarrier);
- double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0;
- [transaction updateData:@{@"count" : @(newCount)} forDocument:doc];
- return @YES;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- [expectation fulfill];
- }];
- }
- [self awaitExpectations];
- // Now all transaction should be completed, so check the result.
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]);
- XCTAssertEqualObjects(@"yes", snapshot[@"other"]);
- }
- - (void)testHandleReadingOneDocAndWritingAnother {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc1 = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
- FIRDocumentReference *doc2 = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
- [self writeDocumentRef:doc1 data:@{@"count" : @(15.0)}];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- // Get the first doc.
- [transaction getDocument:doc1 error:error];
- XCTAssertNil(*error);
- // Do a write outside of the transaction. The first time the
- // transaction is tried, this will bump the version, which
- // will cause the write to doc2 to fail. The second time, it
- // will be a no-op and not bump the version.
- dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0);
- [doc1 setData:@{
- @"count" : @(1234)
- }
- completion:^(NSError *_Nullable error) {
- dispatch_semaphore_signal(writeSemaphore);
- }];
- // We can block on it, because transactions run on a background queue.
- dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER);
- // Now try to update the other doc from within the transaction.
- // This should fail once, because we read 15 earlier.
- [transaction setData:@{@"count" : @(16)} forDocument:doc2];
- return nil;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- // We currently require every document read to also be written.
- // TODO(b/34879758): Add this check back once we drop that.
- // NSError *error = nil;
- // FIRDocument *snapshot = [transaction getDocument:doc1 error:&error];
- // XCTAssertNil(error);
- // XCTAssertEquals(0, tries);
- // XCTAssertEqualObjects(@(1234), snapshot[@"count"]);
- // snapshot = [transaction getDocument:doc2 error:&error];
- // XCTAssertNil(error);
- // XCTAssertEqualObjects(@(16), snapshot[@"count"]);
- XCTAssertNotNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- }
- - (void)testReadingADocTwiceWithDifferentVersions {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"count" : @(15.0)}];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- // Get the doc once.
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertNil(*error);
- XCTAssertEqualObjects(@(15), snapshot[@"count"]);
- // Do a write outside of the transaction.
- dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0);
- [doc setData:@{
- @"count" : @(1234)
- }
- completion:^(NSError *_Nullable error) {
- dispatch_semaphore_signal(writeSemaphore);
- }];
- // We can block on it, because transactions run on a background queue.
- dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER);
- // Get the doc again in the transaction with the new version.
- snapshot = [transaction getDocument:doc error:error];
- // The get itself will fail, because we already read an earlier version of this document.
- // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the
- // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same
- // do multiple times. But they need to handle read errors anyway.
- XCTAssertNotNil(*error);
- return nil;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- [expectation fulfill];
- }];
- [self awaitExpectations];
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]);
- }
- - (void)testCannotHaveAGetWithoutMutations {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID];
- [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
- XCTAssertTrue(snapshot.exists);
- XCTAssertNil(*error);
- return nil;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- // We currently require every document read to also be written.
- // TODO(b/34879758): Fix this check once we drop that requirement.
- XCTAssertNotNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- }
- - (void)testSuccessWithNoTransactionOperations {
- FIRFirestore *firestore = [self firestore];
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- return @"yes";
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertEqualObjects(@"yes", result);
- XCTAssertNil(error);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- }
- - (void)testCancellationOnError {
- FIRFirestore *firestore = [self firestore];
- FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
- __block volatile int32_t count = 0;
- XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
- [firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- OSAtomicIncrement32(&count);
- [transaction setData:@{@"foo" : @"bar"} forDocument:doc];
- if (error) {
- *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}];
- }
- return nil;
- }
- completion:^(id _Nullable result, NSError *_Nullable error) {
- XCTAssertNil(result);
- XCTAssertNotNil(error);
- XCTAssertEqual(35, error.code);
- [expectation fulfill];
- }];
- [self awaitExpectations];
- XCTAssertEqual(1, (int)count);
- FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
- XCTAssertFalse(snapshot.exists);
- }
- - (void)testUpdateFieldsWithDotsTransactionally {
- FIRDocumentReference *doc = [self documentRef];
- XCTestExpectation *expectation =
- [self expectationWithDescription:@"testUpdateFieldsWithDotsTransactionally"];
- [doc.firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- XCTAssertNil(*error);
- [transaction setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc];
- [transaction updateData:@{
- [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"
- }
- forDocument:doc];
- return nil;
- }
- completion:^(id result, NSError *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)testUpdateNestedFieldsTransactionally {
- FIRDocumentReference *doc = [self documentRef];
- XCTestExpectation *expectation =
- [self expectationWithDescription:@"testUpdateNestedFieldsTransactionally"];
- [doc.firestore
- runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
- XCTAssertNil(*error);
- [transaction setData:@{
- @"a" : @{@"b" : @"old"},
- @"c" : @{@"d" : @"old"},
- @"e" : @{@"f" : @"old"}
- }
- forDocument:doc];
- [transaction updateData:@{
- @"a.b" : @"new",
- [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
- }
- forDocument:doc];
- return nil;
- }
- completion:^(id result, NSError *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
|