Skip to content

Commit 6955146

Browse files
author
Brian Chen
authored
Fix preconditions in transactions (#623)
1 parent c091675 commit 6955146

File tree

2 files changed

+299
-182
lines changed

2 files changed

+299
-182
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java

Lines changed: 216 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import com.google.firebase.firestore.FirebaseFirestoreException.Code;
3232
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
3333
import java.util.ArrayList;
34+
import java.util.Arrays;
35+
import java.util.List;
3436
import java.util.Map;
3537
import java.util.concurrent.CountDownLatch;
3638
import java.util.concurrent.atomic.AtomicInteger;
@@ -40,6 +42,160 @@
4042

4143
@RunWith(AndroidJUnit4.class)
4244
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+
}
43199

44200
@After
45201
public void tearDown() {
@@ -60,50 +216,75 @@ public void testGetDocuments() {
60216
}
61217

62218
@Test
63-
public void testDeleteDocument() {
219+
public void testRunsTransactionsAfterGettingExistingDoc() {
64220
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"));
77234
}
78235

79236
@Test
80-
public void testGetNonexistentDocumentThenCreate() {
237+
public void testRunsTransactionsAfterGettingNonexistentDoc() {
81238
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"));
93252
}
94253

95254
@Test
96-
public void testWriteDocumentTwice() {
255+
public void testRunsTransactionsOnExistingDoc() {
97256
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"));
107288
}
108289

109290
@Test
@@ -163,82 +344,6 @@ public void testIncrementTransactionally() {
163344
assertEquals(8, snapshot.getDouble("count").intValue());
164345
}
165346

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-
242347
@Test
243348
public void testTransactionRaisesErrorsForInvalidUpdates() {
244349
final FirebaseFirestore firestore = testFirestore();

0 commit comments

Comments
 (0)