@@ -69,20 +69,29 @@ void runStage(Transaction transaction, DocumentReference docRef)
69
69
70
70
private static TransactionStage get = Transaction ::get ;
71
71
72
+ private enum FromDocumentType {
73
+ // The operation will be performed on a document that exists.
74
+ EXISTING ,
75
+ // The operation will be performed on a document that has never existed.
76
+ NON_EXISTENT ,
77
+ // The operation will be performed on a document that existed, but was deleted.
78
+ DELETED ,
79
+ }
80
+
72
81
/**
73
82
* Used for testing that all possible combinations of executing transactions result in the desired
74
83
* document value or error.
75
84
*
76
- * <p>`run()`, `withExistingDoc()`, and `withNonexistentDoc ()` don't actually do anything except
77
- * assign variables into the TransactionTester.
85
+ * <p>`run()`, `withExistingDoc()`,`withNonexistentDoc()` and `withDeletedDoc ()` don't actually do
86
+ * anything except assign variables into the TransactionTester.
78
87
*
79
88
* <p>`expectDoc()`, `expectNoDoc()`, and `expectError()` will trigger the transaction to run and
80
89
* assert that the end result matches the input.
81
90
*/
82
91
private static class TransactionTester {
83
92
private FirebaseFirestore db ;
84
93
private DocumentReference docRef ;
85
- private boolean fromExistingDoc = false ;
94
+ private FromDocumentType fromDocumentType = FromDocumentType . NON_EXISTENT ;
86
95
private List <TransactionStage > stages = new ArrayList <>();
87
96
88
97
TransactionTester (FirebaseFirestore inputDb ) {
@@ -91,13 +100,19 @@ private static class TransactionTester {
91
100
92
101
@ CanIgnoreReturnValue
93
102
public TransactionTester withExistingDoc () {
94
- fromExistingDoc = true ;
103
+ fromDocumentType = FromDocumentType . EXISTING ;
95
104
return this ;
96
105
}
97
106
98
107
@ CanIgnoreReturnValue
99
108
public TransactionTester withNonexistentDoc () {
100
- fromExistingDoc = false ;
109
+ fromDocumentType = FromDocumentType .NON_EXISTENT ;
110
+ return this ;
111
+ }
112
+
113
+ @ CanIgnoreReturnValue
114
+ public TransactionTester withDeletedDoc () {
115
+ fromDocumentType = FromDocumentType .DELETED ;
101
116
return this ;
102
117
}
103
118
@@ -160,8 +175,20 @@ private void expectError(Code expected) {
160
175
161
176
private void prepareDoc () {
162
177
docRef = db .collection ("tester-docref" ).document ();
163
- if (fromExistingDoc ) {
164
- waitFor (docRef .set (map ("foo" , "bar0" )));
178
+
179
+ switch (fromDocumentType ) {
180
+ case EXISTING :
181
+ waitFor (docRef .set (map ("foo" , "bar0" )));
182
+ break ;
183
+ case NON_EXISTENT :
184
+ // Nothing to do; document does not exist.
185
+ break ;
186
+ case DELETED :
187
+ waitFor (docRef .set (map ("foo" , "bar0" )));
188
+ waitFor (docRef .delete ());
189
+ break ;
190
+ default :
191
+ throw new Error ("invalid fromDocumentType: " + fromDocumentType );
165
192
}
166
193
}
167
194
@@ -223,6 +250,11 @@ public void testRunsTransactionsAfterGettingExistingDoc() {
223
250
tt .withExistingDoc ().run (get , set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
224
251
}
225
252
253
+ // This test is identical to the test above, except that withNonexistentDoc()
254
+ // is replaced by withDeletedDoc(), to guard against regression of
255
+ // https://github.com/firebase/firebase-js-sdk/issues/5871, where transactions
256
+ // would incorrectly fail with FAILED_PRECONDITION when operations were
257
+ // performed on a deleted document (rather than a non-existent document).
226
258
@ Test
227
259
public void testRunsTransactionsAfterGettingNonexistentDoc () {
228
260
FirebaseFirestore firestore = testFirestore ();
@@ -241,6 +273,24 @@ public void testRunsTransactionsAfterGettingNonexistentDoc() {
241
273
tt .withNonexistentDoc ().run (get , set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
242
274
}
243
275
276
+ @ Test
277
+ public void testRunsTransactionsAfterGettingDeletedDoc () {
278
+ FirebaseFirestore firestore = testFirestore ();
279
+ TransactionTester tt = new TransactionTester (firestore );
280
+
281
+ tt .withDeletedDoc ().run (get , delete1 , delete1 ).expectNoDoc ();
282
+ tt .withDeletedDoc ().run (get , delete1 , update2 ).expectError (Code .INVALID_ARGUMENT );
283
+ tt .withDeletedDoc ().run (get , delete1 , set2 ).expectDoc (map ("foo" , "bar2" ));
284
+
285
+ tt .withDeletedDoc ().run (get , update1 , delete1 ).expectError (Code .INVALID_ARGUMENT );
286
+ tt .withDeletedDoc ().run (get , update1 , update2 ).expectError (Code .INVALID_ARGUMENT );
287
+ tt .withDeletedDoc ().run (get , update1 , set2 ).expectError (Code .INVALID_ARGUMENT );
288
+
289
+ tt .withDeletedDoc ().run (get , set1 , delete1 ).expectNoDoc ();
290
+ tt .withDeletedDoc ().run (get , set1 , update2 ).expectDoc (map ("foo" , "bar2" ));
291
+ tt .withDeletedDoc ().run (get , set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
292
+ }
293
+
244
294
@ Test
245
295
public void testRunsTransactionsOnExistingDoc () {
246
296
FirebaseFirestore firestore = testFirestore ();
@@ -277,6 +327,24 @@ public void testRunsTransactionsOnNonexistentDoc() {
277
327
tt .withNonexistentDoc ().run (set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
278
328
}
279
329
330
+ @ Test
331
+ public void testRunsTransactionsOnDeletedDoc () {
332
+ FirebaseFirestore firestore = testFirestore ();
333
+ TransactionTester tt = new TransactionTester (firestore );
334
+
335
+ tt .withDeletedDoc ().run (delete1 , delete1 ).expectNoDoc ();
336
+ tt .withDeletedDoc ().run (delete1 , update2 ).expectError (Code .INVALID_ARGUMENT );
337
+ tt .withDeletedDoc ().run (delete1 , set2 ).expectDoc (map ("foo" , "bar2" ));
338
+
339
+ tt .withDeletedDoc ().run (update1 , delete1 ).expectError (Code .NOT_FOUND );
340
+ tt .withDeletedDoc ().run (update1 , update2 ).expectError (Code .NOT_FOUND );
341
+ tt .withDeletedDoc ().run (update1 , set2 ).expectError (Code .NOT_FOUND );
342
+
343
+ tt .withDeletedDoc ().run (set1 , delete1 ).expectNoDoc ();
344
+ tt .withDeletedDoc ().run (set1 , update2 ).expectDoc (map ("foo" , "bar2" ));
345
+ tt .withDeletedDoc ().run (set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
346
+ }
347
+
280
348
@ Test
281
349
public void testSetDocumentWithMerge () {
282
350
FirebaseFirestore firestore = testFirestore ();
@@ -637,6 +705,29 @@ public void testDoesNotRetryOnPermanentError() {
637
705
assertEquals (1 , count .get ());
638
706
}
639
707
708
+ @ Test
709
+ public void testRetryOnAlreadyExistsError () {
710
+ final FirebaseFirestore firestore = testFirestore ();
711
+ DocumentReference doc = firestore .collection ("foo" ).document ();
712
+ CountDownLatch latch = new CountDownLatch (2 );
713
+ waitFor (
714
+ firestore .runTransaction (
715
+ transaction -> {
716
+ latch .countDown ();
717
+ transaction .get (doc );
718
+ // Do a write outside of the transaction.
719
+ waitFor (doc .set (map ("count" , 1 )));
720
+ // Now try to set the doc within the transaction.This should fail once
721
+ // with ALREADY_EXISTS error.
722
+ transaction .set (doc , map ("count" , 2 ));
723
+ return null ;
724
+ }));
725
+ DocumentSnapshot snapshot = waitFor (doc .get ());
726
+ assertEquals (0 , latch .getCount ());
727
+ assertTrue (snapshot .exists ());
728
+ assertEquals (2L , snapshot .getData ().get ("count" ));
729
+ }
730
+
640
731
@ Test
641
732
public void testMakesDefaultMaxAttempts () {
642
733
FirebaseFirestore firestore = testFirestore ();
0 commit comments