@@ -69,20 +69,29 @@ void runStage(Transaction transaction, DocumentReference docRef)
6969
7070 private static TransactionStage get = Transaction ::get ;
7171
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+
7281 /**
7382 * Used for testing that all possible combinations of executing transactions result in the desired
7483 * document value or error.
7584 *
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.
7887 *
7988 * <p>`expectDoc()`, `expectNoDoc()`, and `expectError()` will trigger the transaction to run and
8089 * assert that the end result matches the input.
8190 */
8291 private static class TransactionTester {
8392 private FirebaseFirestore db ;
8493 private DocumentReference docRef ;
85- private boolean fromExistingDoc = false ;
94+ private FromDocumentType fromDocumentType = FromDocumentType . NON_EXISTENT ;
8695 private List <TransactionStage > stages = new ArrayList <>();
8796
8897 TransactionTester (FirebaseFirestore inputDb ) {
@@ -91,13 +100,19 @@ private static class TransactionTester {
91100
92101 @ CanIgnoreReturnValue
93102 public TransactionTester withExistingDoc () {
94- fromExistingDoc = true ;
103+ fromDocumentType = FromDocumentType . EXISTING ;
95104 return this ;
96105 }
97106
98107 @ CanIgnoreReturnValue
99108 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 ;
101116 return this ;
102117 }
103118
@@ -160,8 +175,20 @@ private void expectError(Code expected) {
160175
161176 private void prepareDoc () {
162177 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 );
165192 }
166193 }
167194
@@ -241,6 +268,29 @@ public void testRunsTransactionsAfterGettingNonexistentDoc() {
241268 tt .withNonexistentDoc ().run (get , set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
242269 }
243270
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+
244294 @ Test
245295 public void testRunsTransactionsOnExistingDoc () {
246296 FirebaseFirestore firestore = testFirestore ();
@@ -277,6 +327,24 @@ public void testRunsTransactionsOnNonexistentDoc() {
277327 tt .withNonexistentDoc ().run (set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
278328 }
279329
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+
280348 @ Test
281349 public void testSetDocumentWithMerge () {
282350 FirebaseFirestore firestore = testFirestore ();
@@ -637,6 +705,29 @@ public void testDoesNotRetryOnPermanentError() {
637705 assertEquals (1 , count .get ());
638706 }
639707
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+
640731 @ Test
641732 public void testMakesDefaultMaxAttempts () {
642733 FirebaseFirestore firestore = testFirestore ();
0 commit comments