@@ -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
86
+ * do 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 RuntimeException ("invalid fromDocumentType: " + fromDocumentType );
165
192
}
166
193
}
167
194
@@ -241,6 +268,29 @@ public void testRunsTransactionsAfterGettingNonexistentDoc() {
241
268
tt .withNonexistentDoc ().run (get , set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
242
269
}
243
270
271
+ // This test is identical to the test above, except that withNonexistentDoc()
272
+ // is replaced by withDeletedDoc(), to guard against regression of
273
+ // https://github.com/firebase/firebase-js-sdk/issues/5871, where transactions
274
+ // would incorrectly fail with FAILED_PRECONDITION when operations were
275
+ // performed on a deleted document (rather than a non-existent document).
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
+ AtomicInteger transactionCallbackCount = new AtomicInteger (0 );
713
+ waitFor (
714
+ firestore .runTransaction (
715
+ transaction -> {
716
+ int currentCount = transactionCallbackCount .incrementAndGet ();
717
+ transaction .get (doc );
718
+ // Do a write outside of the transaction.
719
+ if (currentCount == 1 ) waitFor (doc .set (map ("foo1" , "bar1" )));
720
+ // Now try to set the doc within the transaction. This should fail once
721
+ // with ALREADY_EXISTS error.
722
+ transaction .set (doc , map ("foo2" , "bar2" ));
723
+ return null ;
724
+ }));
725
+ DocumentSnapshot snapshot = waitFor (doc .get ());
726
+ assertEquals (2 , transactionCallbackCount .get ());
727
+ assertTrue (snapshot .exists ());
728
+ assertEquals (map ("foo2" , "bar2" ), snapshot .getData ());
729
+ }
730
+
640
731
@ Test
641
732
public void testMakesDefaultMaxAttempts () {
642
733
FirebaseFirestore firestore = testFirestore ();
0 commit comments