@@ -104,6 +104,15 @@ - (void)assertSnapshot:(FIRDocumentSnapshot *)snapshot
104
104
[transaction getDocument: doc error: &error];
105
105
};
106
106
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
+
107
116
/* *
108
117
* Used for testing that all possible combinations of executing transactions result in the desired
109
118
* document value or error.
@@ -117,14 +126,15 @@ - (void)assertSnapshot:(FIRDocumentSnapshot *)snapshot
117
126
@interface FSTTransactionTester : NSObject
118
127
- (FSTTransactionTester *)withExistingDoc ;
119
128
- (FSTTransactionTester *)withNonexistentDoc ;
129
+ - (FSTTransactionTester *)withDeletedDoc ;
120
130
- (FSTTransactionTester *)runWithStages : (NSArray <TransactionStage> *)stages ;
121
131
- (void )expectDoc : (NSObject *)expected ;
122
132
- (void )expectNoDoc ;
123
133
- (void )expectError : (FIRFirestoreErrorCode)expected ;
124
134
125
135
@property (atomic , strong , readwrite ) NSArray <TransactionStage> *stages;
126
136
@property (atomic , strong , readwrite ) FIRDocumentReference *docRef;
127
- @property (atomic , assign , readwrite ) BOOL fromExistingDoc ;
137
+ @property (atomic , assign , readwrite ) FIRFromDocumentType fromDocumentType ;
128
138
@end
129
139
130
140
@implementation FSTTransactionTester {
@@ -137,19 +147,25 @@ - (instancetype)initWithDb:(FIRFirestore *)db testCase:(FSTTransactionTests *)te
137
147
if (self) {
138
148
_db = db;
139
149
_stages = [NSArray array ];
150
+ _fromDocumentType = FIRFromDocumentTypeNonExistent;
140
151
_testCase = testCase;
141
152
_testExpectations = [NSMutableArray array ];
142
153
}
143
154
return self;
144
155
}
145
156
146
157
- (FSTTransactionTester *)withExistingDoc {
147
- self.fromExistingDoc = YES ;
158
+ self.fromDocumentType = FIRFromDocumentTypeExisting ;
148
159
return self;
149
160
}
150
161
151
162
- (FSTTransactionTester *)withNonexistentDoc {
152
- self.fromExistingDoc = NO ;
163
+ self.fromDocumentType = FIRFromDocumentTypeNonExistent;
164
+ return self;
165
+ }
166
+
167
+ - (FSTTransactionTester *)withDeletedDoc {
168
+ self.fromDocumentType = FIRFromDocumentTypeDeleted;
153
169
return self;
154
170
}
155
171
@@ -195,10 +211,30 @@ - (void)expectError:(FIRFirestoreErrorCode)expected {
195
211
196
212
- (void )prepareDoc {
197
213
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
+ }
202
238
}
203
239
}
204
240
@@ -215,6 +251,17 @@ - (NSError *)writeDocumentRef:(FIRDocumentReference *)ref
215
251
return errorResult;
216
252
}
217
253
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
+
218
265
- (void )runSuccessfulTransaction {
219
266
XCTestExpectation *expectation =
220
267
[_testCase expectationWithDescription: @" runSuccessfulTransaction" ];
@@ -322,6 +369,32 @@ - (void)testRunsTransactionsAfterGettingNonexistentDoc {
322
369
[[[tt withNonexistentDoc ] runWithStages: @[ get, set1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
323
370
}
324
371
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
+
325
398
- (void )testRunsTransactionOnExistingDoc {
326
399
FIRFirestore *firestore = [self firestore ];
327
400
FSTTransactionTester *tt = [[FSTTransactionTester alloc ] initWithDb: firestore testCase: self ];
@@ -361,6 +434,27 @@ - (void)testRunsTransactionsOnNonexistentDoc {
361
434
[[[tt withNonexistentDoc ] runWithStages: @[ set1, set2 ]] expectDoc: @{@" foo" : @" bar2" }];
362
435
}
363
436
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
+
364
458
- (void )testSetDocumentWithMerge {
365
459
FIRFirestore *firestore = [self firestore ];
366
460
FIRDocumentReference *doc = [[firestore collectionWithPath: @" towns" ] documentWithAutoID ];
@@ -653,6 +747,56 @@ - (void)testDoesNotRetryOnPermanentError {
653
747
[self awaitExpectations ];
654
748
}
655
749
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
+
656
800
- (void )testMakesDefaultMaxAttempts {
657
801
FIRFirestore *firestore = [self firestore ];
658
802
FIRDocumentReference *doc1 = [[firestore collectionWithPath: @" counters" ] documentWithAutoID ];
0 commit comments