31
31
import com .google .firebase .firestore .FirebaseFirestoreException .Code ;
32
32
import com .google .firebase .firestore .testutil .IntegrationTestUtil ;
33
33
import java .util .ArrayList ;
34
+ import java .util .Arrays ;
35
+ import java .util .List ;
34
36
import java .util .Map ;
35
37
import java .util .concurrent .CountDownLatch ;
36
38
import java .util .concurrent .atomic .AtomicInteger ;
40
42
41
43
@ RunWith (AndroidJUnit4 .class )
42
44
public class TransactionTest {
45
+ interface TransactionStage {
46
+ void runStage (Transaction transaction , DocumentReference docRef )
47
+ throws FirebaseFirestoreException ;
48
+ }
49
+
50
+ private static TransactionStage delete1 = Transaction ::delete ;
51
+
52
+ private static TransactionStage update1 =
53
+ (Transaction transaction , DocumentReference docRef ) ->
54
+ transaction .update (docRef , map ("foo" , "bar1" ));
55
+
56
+ private static TransactionStage update2 =
57
+ (Transaction transaction , DocumentReference docRef ) ->
58
+ transaction .update (docRef , map ("foo" , "bar2" ));
59
+
60
+ private static TransactionStage set1 =
61
+ (Transaction transaction , DocumentReference docRef ) ->
62
+ transaction .set (docRef , map ("foo" , "bar1" ));
63
+
64
+ private static TransactionStage set2 =
65
+ (Transaction transaction , DocumentReference docRef ) ->
66
+ transaction .set (docRef , map ("foo" , "bar2" ));
67
+
68
+ private static TransactionStage get = Transaction ::get ;
69
+
70
+ /**
71
+ * Used for testing that all possible combinations of executing transactions result in the desired
72
+ * document value or error.
73
+ *
74
+ * <p>`run()`, `withExistingDoc()`, and `withNonexistentDoc()` don't actually do anything except
75
+ * assign variables into the TransactionTester.
76
+ *
77
+ * <p>`expectDoc()`, `expectNoDoc()`, and `expectError()` will trigger the transaction to run and
78
+ * assert that the end result matches the input.
79
+ */
80
+ private static class TransactionTester {
81
+ private FirebaseFirestore db ;
82
+ private DocumentReference docRef ;
83
+ private boolean fromExistingDoc = false ;
84
+ private List <TransactionStage > stages = new ArrayList <>();
85
+
86
+ TransactionTester (FirebaseFirestore inputDb ) {
87
+ db = inputDb ;
88
+ }
89
+
90
+ public TransactionTester withExistingDoc () {
91
+ fromExistingDoc = true ;
92
+ return this ;
93
+ }
94
+
95
+ public TransactionTester withNonexistentDoc () {
96
+ fromExistingDoc = false ;
97
+ return this ;
98
+ }
99
+
100
+ public TransactionTester run (TransactionStage ... inputStages ) {
101
+ stages = Arrays .asList (inputStages );
102
+ return this ;
103
+ }
104
+
105
+ public void expectDoc (Object expected ) {
106
+ try {
107
+ prepareDoc ();
108
+ waitFor (runTransaction ());
109
+ DocumentSnapshot snapshot = waitFor (docRef .get ());
110
+ assertTrue (snapshot .exists ());
111
+ assertEquals (expected , snapshot .getData ());
112
+ } catch (Exception e ) {
113
+ fail (
114
+ "Expected the sequence ("
115
+ + listStages (stages )
116
+ + ") to succeed, but got "
117
+ + e .toString ());
118
+ }
119
+ cleanupTester ();
120
+ }
121
+
122
+ private void expectNoDoc () {
123
+ try {
124
+ prepareDoc ();
125
+ waitFor (runTransaction ());
126
+ DocumentSnapshot snapshot = waitFor (docRef .get ());
127
+ assertFalse (snapshot .exists ());
128
+ } catch (Exception e ) {
129
+ fail (
130
+ "Expected the sequence ("
131
+ + listStages (stages )
132
+ + ") to succeed, but got "
133
+ + e .toString ());
134
+ }
135
+ cleanupTester ();
136
+ }
137
+
138
+ private void expectError (Code expected ) {
139
+ prepareDoc ();
140
+ Task <Void > transactionTask = runTransaction ();
141
+ try {
142
+ waitForException (transactionTask );
143
+ } catch (Exception e ) {
144
+ throw new AssertionError (
145
+ "Expected the sequence ("
146
+ + listStages (stages )
147
+ + ") to fail with the error "
148
+ + expected .toString ());
149
+ }
150
+ assertFalse (transactionTask .isSuccessful ());
151
+ Exception e = transactionTask .getException ();
152
+ assertEquals (expected , ((FirebaseFirestoreException ) e ).getCode ());
153
+ cleanupTester ();
154
+ }
155
+
156
+ private void prepareDoc () {
157
+ docRef = db .collection ("tester-docref" ).document ();
158
+ if (fromExistingDoc ) {
159
+ docRef .set (map ("foo" , "bar0" ));
160
+ DocumentSnapshot docSnap = waitFor (docRef .get ());
161
+ assertTrue (docSnap .exists ());
162
+ }
163
+ }
164
+
165
+ private Task <Void > runTransaction () {
166
+ return db .runTransaction (
167
+ transaction -> {
168
+ for (TransactionStage stage : stages ) {
169
+ stage .runStage (transaction , docRef );
170
+ }
171
+ return null ;
172
+ });
173
+ }
174
+
175
+ private void cleanupTester () {
176
+ stages = new ArrayList <>();
177
+ // Set the docRef to something else to lose the original reference.
178
+ docRef = db .collection ("reset" ).document ();
179
+ }
180
+
181
+ private static String listStages (List <TransactionStage > stages ) {
182
+ List <String > seqList = new ArrayList <>();
183
+ for (TransactionStage stage : stages ) {
184
+ if (stage == delete1 ) {
185
+ seqList .add ("delete" );
186
+ } else if (stage == update1 || stage == update2 ) {
187
+ seqList .add ("update" );
188
+ } else if (stage == set1 || stage == set2 ) {
189
+ seqList .add ("set" );
190
+ } else if (stage == get ) {
191
+ seqList .add ("get" );
192
+ } else {
193
+ throw new IllegalArgumentException ("Stage not recognized" );
194
+ }
195
+ }
196
+ return seqList .toString ();
197
+ }
198
+ }
43
199
44
200
@ After
45
201
public void tearDown () {
@@ -60,50 +216,75 @@ public void testGetDocuments() {
60
216
}
61
217
62
218
@ Test
63
- public void testDeleteDocument () {
219
+ public void testRunsTransactionsAfterGettingExistingDoc () {
64
220
FirebaseFirestore firestore = testFirestore ();
65
- DocumentReference doc = firestore .collection ("towns" ).document ();
66
- waitFor (doc .set (map ("foo" , "bar" )));
67
- DocumentSnapshot snapshot = waitFor (doc .get ());
68
- assertEquals ("bar" , snapshot .getString ("foo" ));
69
- waitFor (
70
- firestore .runTransaction (
71
- transaction -> {
72
- transaction .delete (doc );
73
- return null ;
74
- }));
75
- snapshot = waitFor (doc .get ());
76
- assertFalse (snapshot .exists ());
221
+ TransactionTester tt = new TransactionTester (firestore );
222
+
223
+ tt .withExistingDoc ().run (get , delete1 , delete1 ).expectNoDoc ();
224
+ tt .withExistingDoc ().run (get , delete1 , update2 ).expectError (Code .INVALID_ARGUMENT );
225
+ tt .withExistingDoc ().run (get , delete1 , set2 ).expectDoc (map ("foo" , "bar2" ));
226
+
227
+ tt .withExistingDoc ().run (get , update1 , delete1 ).expectNoDoc ();
228
+ tt .withExistingDoc ().run (get , update1 , update2 ).expectDoc (map ("foo" , "bar2" ));
229
+ tt .withExistingDoc ().run (get , update1 , set2 ).expectDoc (map ("foo" , "bar2" ));
230
+
231
+ tt .withExistingDoc ().run (get , set1 , delete1 ).expectNoDoc ();
232
+ tt .withExistingDoc ().run (get , set1 , update2 ).expectDoc (map ("foo" , "bar2" ));
233
+ tt .withExistingDoc ().run (get , set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
77
234
}
78
235
79
236
@ Test
80
- public void testGetNonexistentDocumentThenCreate () {
237
+ public void testRunsTransactionsAfterGettingNonexistentDoc () {
81
238
FirebaseFirestore firestore = testFirestore ();
82
- DocumentReference docRef = firestore .collection ("towns" ).document ();
83
- waitFor (
84
- firestore .runTransaction (
85
- transaction -> {
86
- DocumentSnapshot docSnap = transaction .get (docRef );
87
- assertFalse (docSnap .exists ());
88
- transaction .set (docRef , map ("foo" , "bar" ));
89
- return null ;
90
- }));
91
- DocumentSnapshot snapshot = waitFor (docRef .get ());
92
- assertEquals ("bar" , snapshot .getString ("foo" ));
239
+ TransactionTester tt = new TransactionTester (firestore );
240
+
241
+ tt .withNonexistentDoc ().run (get , delete1 , delete1 ).expectNoDoc ();
242
+ tt .withNonexistentDoc ().run (get , delete1 , update2 ).expectError (Code .INVALID_ARGUMENT );
243
+ tt .withNonexistentDoc ().run (get , delete1 , set2 ).expectDoc (map ("foo" , "bar2" ));
244
+
245
+ tt .withNonexistentDoc ().run (get , update1 , delete1 ).expectError (Code .INVALID_ARGUMENT );
246
+ tt .withNonexistentDoc ().run (get , update1 , update2 ).expectError (Code .INVALID_ARGUMENT );
247
+ tt .withNonexistentDoc ().run (get , update1 , set2 ).expectError (Code .INVALID_ARGUMENT );
248
+
249
+ tt .withNonexistentDoc ().run (get , set1 , delete1 ).expectNoDoc ();
250
+ tt .withNonexistentDoc ().run (get , set1 , update2 ).expectDoc (map ("foo" , "bar2" ));
251
+ tt .withNonexistentDoc ().run (get , set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
93
252
}
94
253
95
254
@ Test
96
- public void testWriteDocumentTwice () {
255
+ public void testRunsTransactionsOnExistingDoc () {
97
256
FirebaseFirestore firestore = testFirestore ();
98
- DocumentReference doc = firestore .collection ("towns" ).document ();
99
- waitFor (
100
- firestore .runTransaction (
101
- transaction -> {
102
- transaction .set (doc , map ("a" , "b" )).set (doc , map ("c" , "d" ));
103
- return null ;
104
- }));
105
- DocumentSnapshot snapshot = waitFor (doc .get ());
106
- assertEquals (map ("c" , "d" ), snapshot .getData ());
257
+ TransactionTester tt = new TransactionTester (firestore );
258
+
259
+ tt .withExistingDoc ().run (delete1 , delete1 ).expectNoDoc ();
260
+ tt .withExistingDoc ().run (delete1 , update2 ).expectError (Code .INVALID_ARGUMENT );
261
+ tt .withExistingDoc ().run (delete1 , set2 ).expectDoc (map ("foo" , "bar2" ));
262
+
263
+ tt .withExistingDoc ().run (update1 , delete1 ).expectNoDoc ();
264
+ tt .withExistingDoc ().run (update1 , update2 ).expectDoc (map ("foo" , "bar2" ));
265
+ tt .withExistingDoc ().run (update1 , set2 ).expectDoc (map ("foo" , "bar2" ));
266
+
267
+ tt .withExistingDoc ().run (set1 , delete1 ).expectNoDoc ();
268
+ tt .withExistingDoc ().run (set1 , update2 ).expectDoc (map ("foo" , "bar2" ));
269
+ tt .withExistingDoc ().run (set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
270
+ }
271
+
272
+ @ Test
273
+ public void testRunsTransactionsOnNonexistentDoc () {
274
+ FirebaseFirestore firestore = testFirestore ();
275
+ TransactionTester tt = new TransactionTester (firestore );
276
+
277
+ tt .withNonexistentDoc ().run (delete1 , delete1 ).expectNoDoc ();
278
+ tt .withNonexistentDoc ().run (delete1 , update2 ).expectError (Code .INVALID_ARGUMENT );
279
+ tt .withNonexistentDoc ().run (delete1 , set2 ).expectDoc (map ("foo" , "bar2" ));
280
+
281
+ tt .withNonexistentDoc ().run (update1 , delete1 ).expectError (Code .NOT_FOUND );
282
+ tt .withNonexistentDoc ().run (update1 , update2 ).expectError (Code .NOT_FOUND );
283
+ tt .withNonexistentDoc ().run (update1 , set2 ).expectError (Code .NOT_FOUND );
284
+
285
+ tt .withNonexistentDoc ().run (set1 , delete1 ).expectNoDoc ();
286
+ tt .withNonexistentDoc ().run (set1 , update2 ).expectDoc (map ("foo" , "bar2" ));
287
+ tt .withNonexistentDoc ().run (set1 , set2 ).expectDoc (map ("foo" , "bar2" ));
107
288
}
108
289
109
290
@ Test
@@ -163,82 +344,6 @@ public void testIncrementTransactionally() {
163
344
assertEquals (8 , snapshot .getDouble ("count" ).intValue ());
164
345
}
165
346
166
- @ Test
167
- public void testTransactionRejectsUpdatesForNonexistentDocuments () {
168
- final FirebaseFirestore firestore = testFirestore ();
169
-
170
- // Make a transaction that will fail
171
- Task <Void > transactionTask =
172
- firestore .runTransaction (
173
- transaction -> {
174
- // Get and update a document that doesn't exist so that the transaction fails
175
- DocumentSnapshot doc =
176
- transaction .get (firestore .collection ("nonexistent" ).document ());
177
- transaction .update (doc .getReference (), "foo" , "bar" );
178
- return null ;
179
- });
180
-
181
- // Let all of the transactions fetch the old value and stop once.
182
- waitForException (transactionTask );
183
- assertFalse (transactionTask .isSuccessful ());
184
- Exception e = transactionTask .getException ();
185
- assertEquals (Code .INVALID_ARGUMENT , ((FirebaseFirestoreException ) e ).getCode ());
186
- assertEquals ("Can't update a document that doesn't exist." , e .getMessage ());
187
- }
188
-
189
- @ Test
190
- public void testCantDeleteDocumentThenPatch () {
191
- final FirebaseFirestore firestore = testFirestore ();
192
- final DocumentReference docRef = firestore .collection ("docs" ).document ();
193
- waitFor (docRef .set (map ("foo" , "bar" )));
194
-
195
- // Make a transaction that will fail
196
- Task <Void > transactionTask =
197
- firestore .runTransaction (
198
- transaction -> {
199
- DocumentSnapshot doc = transaction .get (docRef );
200
- assertTrue (doc .exists ());
201
- transaction .delete (docRef );
202
- // Since we deleted the doc, the update will fail
203
- transaction .update (docRef , "foo" , "bar" );
204
- return null ;
205
- });
206
-
207
- // Let all of the transactions fetch the old value and stop once.
208
- waitForException (transactionTask );
209
- assertFalse (transactionTask .isSuccessful ());
210
- Exception e = transactionTask .getException ();
211
- assertEquals (Code .INVALID_ARGUMENT , ((FirebaseFirestoreException ) e ).getCode ());
212
- assertEquals ("Can't update a document that doesn't exist." , e .getMessage ());
213
- }
214
-
215
- @ Test
216
- public void testCantDeleteDocumentThenSet () {
217
- final FirebaseFirestore firestore = testFirestore ();
218
- final DocumentReference docRef = firestore .collection ("docs" ).document ();
219
- waitFor (docRef .set (map ("foo" , "bar" )));
220
-
221
- // Make a transaction that will fail
222
- Task <Void > transactionTask =
223
- firestore .runTransaction (
224
- transaction -> {
225
- DocumentSnapshot doc = transaction .get (docRef );
226
- assertTrue (doc .exists ());
227
- transaction .delete (docRef );
228
- // TODO: In theory this should work, but it's complex to make it work, so
229
- // instead we just let the transaction fail and verify it's unsupported for now
230
- transaction .set (docRef , map ("foo" , "new-bar" ));
231
- return null ;
232
- });
233
-
234
- // Let all of the transactions fetch the old value and stop once.
235
- waitForException (transactionTask );
236
- assertFalse (transactionTask .isSuccessful ());
237
- Exception e = transactionTask .getException ();
238
- // This is the error surfaced by the backend.
239
- assertEquals (Code .INVALID_ARGUMENT , ((FirebaseFirestoreException ) e ).getCode ());
240
- }
241
-
242
347
@ Test
243
348
public void testTransactionRaisesErrorsForInvalidUpdates () {
244
349
final FirebaseFirestore firestore = testFirestore ();
0 commit comments