@@ -104,6 +104,15 @@ - (void)assertSnapshot:(FIRDocumentSnapshot *)snapshot
104104 [transaction getDocument: doc error: &error];
105105};
106106
107+ typedef NS_ENUM (NSUInteger , FIRFromDocumentType) {
108+ // The operation will be performed on a document that exists.
109+ FIRFromDocumentTypeExisting,
110+ // The operation will be performed on a document that has never existed.
111+ FIRFromDocumentTypeNonExistent,
112+ // The operation will be performed on a document that existed, but was deleted.
113+ FIRFromDocumentTypeDeleted,
114+ };
115+
107116/* *
108117 * Used for testing that all possible combinations of executing transactions result in the desired
109118 * document value or error.
@@ -117,14 +126,15 @@ - (void)assertSnapshot:(FIRDocumentSnapshot *)snapshot
117126@interface FSTTransactionTester : NSObject
118127- (FSTTransactionTester *)withExistingDoc ;
119128- (FSTTransactionTester *)withNonexistentDoc ;
129+ - (FSTTransactionTester *)withDeletedDoc ;
120130- (FSTTransactionTester *)runWithStages : (NSArray <TransactionStage> *)stages ;
121131- (void )expectDoc : (NSObject *)expected ;
122132- (void )expectNoDoc ;
123133- (void )expectError : (FIRFirestoreErrorCode)expected ;
124134
125135@property (atomic , strong , readwrite ) NSArray <TransactionStage> *stages;
126136@property (atomic , strong , readwrite ) FIRDocumentReference *docRef;
127- @property (atomic , assign , readwrite ) BOOL fromExistingDoc ;
137+ @property (atomic , assign , readwrite ) FIRFromDocumentType fromDocumentType ;
128138@end
129139
130140@implementation FSTTransactionTester {
@@ -137,19 +147,25 @@ - (instancetype)initWithDb:(FIRFirestore *)db testCase:(FSTTransactionTests *)te
137147 if (self) {
138148 _db = db;
139149 _stages = [NSArray array ];
150+ _fromDocumentType = FIRFromDocumentTypeNonExistent;
140151 _testCase = testCase;
141152 _testExpectations = [NSMutableArray array ];
142153 }
143154 return self;
144155}
145156
146157- (FSTTransactionTester *)withExistingDoc {
147- self.fromExistingDoc = YES ;
158+ self.fromDocumentType = FIRFromDocumentTypeExisting ;
148159 return self;
149160}
150161
151162- (FSTTransactionTester *)withNonexistentDoc {
152- self.fromExistingDoc = NO ;
163+ self.fromDocumentType = FIRFromDocumentTypeNonExistent;
164+ return self;
165+ }
166+
167+ - (FSTTransactionTester *)withDeletedDoc {
168+ self.fromDocumentType = FIRFromDocumentTypeDeleted;
153169 return self;
154170}
155171
@@ -195,10 +211,30 @@ - (void)expectError:(FIRFirestoreErrorCode)expected {
195211
196212- (void )prepareDoc {
197213 self.docRef = [[_db collectionWithPath: @" nonexistent" ] documentWithAutoID ];
198- if (_fromExistingDoc) {
199- NSError *setError = [self writeDocumentRef: self .docRef data: @{@" foo" : @" bar" }];
200- NSString *message = [NSString stringWithFormat: @" Failed set at %@ " , [self stageNames ]];
201- [_testCase assertNilError: setError message: message];
214+ switch (_fromDocumentType) {
215+ case FIRFromDocumentTypeExisting: {
216+ NSError *setError = [self writeDocumentRef: self .docRef data: @{@" foo" : @" bar" }];
217+ NSString *message = [NSString stringWithFormat: @" Failed set at %@ " , [self stageNames ]];
218+ [_testCase assertNilError: setError message: message];
219+ break ;
220+ }
221+ case FIRFromDocumentTypeNonExistent: {
222+ // Nothing to do; document does not exist.
223+ break ;
224+ }
225+ case FIRFromDocumentTypeDeleted: {
226+ {
227+ NSError *setError = [self writeDocumentRef: self .docRef data: @{@" foo" : @" bar" }];
228+ NSString *message = [NSString stringWithFormat: @" Failed set at %@ " , [self stageNames ]];
229+ [_testCase assertNilError: setError message: message];
230+ }
231+ {
232+ NSError *deleteError = [self deleteDocumentRef: self .docRef];
233+ NSString *message = [NSString stringWithFormat: @" Failed delete at %@ " , [self stageNames ]];
234+ [_testCase assertNilError: deleteError message: message];
235+ }
236+ break ;
237+ }
202238 }
203239}
204240
@@ -215,6 +251,17 @@ - (NSError *)writeDocumentRef:(FIRDocumentReference *)ref
215251 return errorResult;
216252}
217253
254+ - (NSError *)deleteDocumentRef : (FIRDocumentReference *)ref {
255+ __block NSError *errorResult;
256+ XCTestExpectation *expectation = [_testCase expectationWithDescription: @" prepareDoc:delete" ];
257+ [ref deleteDocumentWithCompletion: ^(NSError *error) {
258+ errorResult = error;
259+ [expectation fulfill ];
260+ }];
261+ [_testCase awaitExpectations ];
262+ return errorResult;
263+ }
264+
218265- (void )runSuccessfulTransaction {
219266 XCTestExpectation *expectation =
220267 [_testCase expectationWithDescription: @" runSuccessfulTransaction" ];
@@ -322,6 +369,32 @@ - (void)testRunsTransactionsAfterGettingNonexistentDoc {
322369 [[[tt withNonexistentDoc ] runWithStages: @[ get, set1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
323370}
324371
372+ // This test is identical to the test above, except that withNonexistentDoc() is replaced by
373+ // withDeletedDoc(), to guard against regression of
374+ // https://github.com/firebase/firebase-js-sdk/issues/5871, where transactions would incorrectly
375+ // fail with FAILED_PRECONDITION when operations were performed on a deleted document (rather than
376+ // a non-existent document).
377+ - (void )testRunsTransactionsAfterGettingDeletedDoc {
378+ FIRFirestore *firestore = [self firestore ];
379+ FSTTransactionTester *tt = [[FSTTransactionTester alloc ] initWithDb: firestore testCase: self ];
380+
381+ [[[tt withDeletedDoc ] runWithStages: @[ get, delete1, delete1 ]] expectNoDoc ];
382+ [[[tt withDeletedDoc ] runWithStages: @[ get, delete1, update2 ]]
383+ expectError: FIRFirestoreErrorCodeInvalidArgument];
384+ [[[tt withDeletedDoc ] runWithStages: @[ get, delete1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
385+
386+ [[[tt withDeletedDoc ] runWithStages: @[ get, update1, delete1 ]]
387+ expectError: FIRFirestoreErrorCodeInvalidArgument];
388+ [[[tt withDeletedDoc ] runWithStages: @[ get, update1, update2 ]]
389+ expectError: FIRFirestoreErrorCodeInvalidArgument];
390+ [[[tt withDeletedDoc ] runWithStages: @[ get, update1, set2 ]]
391+ expectError: FIRFirestoreErrorCodeInvalidArgument];
392+
393+ [[[tt withDeletedDoc ] runWithStages: @[ get, set1, delete1 ]] expectNoDoc ];
394+ [[[tt withDeletedDoc ] runWithStages: @[ get, set1, update2 ]] expectDoc: @{@" foo" : @" bar2" }];
395+ [[[tt withDeletedDoc ] runWithStages: @[ get, set1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
396+ }
397+
325398- (void )testRunsTransactionOnExistingDoc {
326399 FIRFirestore *firestore = [self firestore ];
327400 FSTTransactionTester *tt = [[FSTTransactionTester alloc ] initWithDb: firestore testCase: self ];
@@ -361,6 +434,27 @@ - (void)testRunsTransactionsOnNonexistentDoc {
361434 [[[tt withNonexistentDoc ] runWithStages: @[ set1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
362435}
363436
437+ - (void )testRunsTransactionsOnDeletedDoc {
438+ FIRFirestore *firestore = [self firestore ];
439+ FSTTransactionTester *tt = [[FSTTransactionTester alloc ] initWithDb: firestore testCase: self ];
440+
441+ [[[tt withDeletedDoc ] runWithStages: @[ delete1, delete1 ]] expectNoDoc ];
442+ [[[tt withDeletedDoc ] runWithStages: @[ delete1, update2 ]]
443+ expectError: FIRFirestoreErrorCodeInvalidArgument];
444+ [[[tt withDeletedDoc ] runWithStages: @[ delete1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
445+
446+ [[[tt withDeletedDoc ] runWithStages: @[ update1, delete1 ]]
447+ expectError: FIRFirestoreErrorCodeNotFound];
448+ [[[tt withDeletedDoc ] runWithStages: @[ update1, update2 ]]
449+ expectError: FIRFirestoreErrorCodeNotFound];
450+ [[[tt withDeletedDoc ] runWithStages: @[ update1, set2 ]]
451+ expectError: FIRFirestoreErrorCodeNotFound];
452+
453+ [[[tt withDeletedDoc ] runWithStages: @[ set1, delete1 ]] expectNoDoc ];
454+ [[[tt withDeletedDoc ] runWithStages: @[ set1, update2 ]] expectDoc: @{@" foo" : @" bar2" }];
455+ [[[tt withDeletedDoc ] runWithStages: @[ set1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
456+ }
457+
364458- (void )testSetDocumentWithMerge {
365459 FIRFirestore *firestore = [self firestore ];
366460 FIRDocumentReference *doc = [[firestore collectionWithPath: @" towns" ] documentWithAutoID ];
@@ -653,6 +747,56 @@ - (void)testDoesNotRetryOnPermanentError {
653747 [self awaitExpectations ];
654748}
655749
750+ - (void )testRetryOnAlreadyExistsError {
751+ FIRFirestore *firestore = [self firestore ];
752+ FIRDocumentReference *doc1 = [[firestore collectionWithPath: @" counters" ] documentWithAutoID ];
753+ auto transactionCallbackCallCount = std::make_shared<std::atomic_int>(0 );
754+
755+ // Skip backoff delays.
756+ [firestore workerQueue ]->SkipDelaysForTimerId (TimerId::RetryTransaction);
757+
758+ XCTestExpectation *expectation = [self expectationWithDescription: @" transaction" ];
759+ [firestore
760+ runTransactionWithBlock: ^id _Nullable (FIRTransaction *transaction, NSError **error) {
761+ int callbackNum = ++(*transactionCallbackCallCount);
762+
763+ FIRDocumentSnapshot *snapshot = [transaction getDocument: doc1 error: error];
764+ XCTAssertNil (*error);
765+
766+ if (callbackNum == 1 ) {
767+ XCTAssertFalse (snapshot.exists );
768+ // Create the document outside of the transaction to cause the commit to fail with
769+ // ALREADY_EXISTS.
770+ dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create (0 );
771+ [doc1 setData: @{@" foo1" : @" bar1" }
772+ completion: ^(NSError *) {
773+ dispatch_semaphore_signal (writeSemaphore);
774+ }];
775+ // We can block on it, because transactions run on a background queue.
776+ dispatch_semaphore_wait (writeSemaphore, DISPATCH_TIME_FOREVER);
777+ } else if (callbackNum == 2 ) {
778+ XCTAssertTrue (snapshot.exists );
779+ } else {
780+ XCTFail (@" unexpected callbackNum: %@ " , @(callbackNum));
781+ }
782+
783+ [transaction setData: @{@" foo2" : @" bar2" } forDocument: doc1];
784+
785+ return nil ;
786+ }
787+ completion: ^(id , NSError *_Nullable error) {
788+ [expectation fulfill ];
789+ XCTAssertNil (error);
790+ }];
791+ [self awaitExpectations ];
792+
793+ XCTAssertEqual (transactionCallbackCallCount->load (), 2 );
794+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef: doc1];
795+ XCTAssertNotNil (snapshot);
796+ XCTAssertTrue (snapshot.exists );
797+ XCTAssertEqualObjects (snapshot.data , (@{@" foo2" : @" bar2" }));
798+ }
799+
656800- (void )testMakesDefaultMaxAttempts {
657801 FIRFirestore *firestore = [self firestore ];
658802 FIRDocumentReference *doc1 = [[firestore collectionWithPath: @" counters" ] documentWithAutoID ];
0 commit comments