/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.database.integration;
import static com.google.firebase.database.TestHelpers.fromSingleQuotedString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.common.collect.Iterables;
import com.google.firebase.FirebaseApp;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseException;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.DatabaseReference.CompletionListener;
import com.google.firebase.database.EventRecord;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.MapBuilder;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.ServerValue;
import com.google.firebase.database.TestChildEventListener;
import com.google.firebase.database.TestFailure;
import com.google.firebase.database.TestHelpers;
import com.google.firebase.database.Transaction;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.database.core.AuthTokenProvider;
import com.google.firebase.database.core.DatabaseConfig;
import com.google.firebase.database.core.RepoManager;
import com.google.firebase.database.future.ReadFuture;
import com.google.firebase.database.future.WriteFuture;
import com.google.firebase.testing.IntegrationTestUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
public class TransactionTestIT {
private static FirebaseApp masterApp;
@BeforeClass
public static void setUpClass() throws IOException {
masterApp = IntegrationTestUtils.ensureDefaultApp();
}
@Before
public void prepareApp() {
TestHelpers.wrapForErrorHandling(masterApp);
}
@After
public void checkAndCleanupApp() {
TestHelpers.assertAndUnwrapErrorHandlers(masterApp);
}
@Test
public void testNewValueIsImmediatelyVisible() throws Exception {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
ref.child("foo").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
try {
currentData.setValue(42);
} catch (DatabaseException e) {
fail("Should not fail");
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
if (error != null || !committed) {
fail("Transaction should succeed");
}
}
});
DataSnapshot snap = new ReadFuture(ref.child("foo")).timedGet().get(0).getSnapshot();
assertEquals(42L, snap.getValue());
}
@Test
public void testEventRaisedForNewValue() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
EventHelper helper = new EventHelper().addValueExpectation(ref).startListening();
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
try {
currentData.setValue(42);
} catch (DatabaseException e) {
fail("Should not throw");
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
// No-op
}
});
assertTrue(helper.waitForEvents());
helper.cleanup();
}
@Test
public void testNonAbortedTransactionSetsCommittedToTrue() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
try {
currentData.setValue(42);
} catch (DatabaseException e) {
fail("Should not fail");
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
if (error != null || !committed) {
fail("Transaction should succeed");
} else {
assertEquals(42L, currentData.getValue());
semaphore.release(1);
}
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testAbortedTransactionSetsCommittedToFalse() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
return Transaction.abort();
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertFalse(committed);
assertNull(currentData.getValue());
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testSetDataReconnectDoTransactionThatAborts() throws
TestFailure, ExecutionException, TimeoutException, InterruptedException {
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
new WriteFuture(refs.get(0), 42).timedGet();
DatabaseReference node = refs.get(1);
// Go offline to ensure our listen doesn't complete before the transaction
// runs.
node.getDatabase().goOffline();
final AtomicInteger count = new AtomicInteger(0);
ReadFuture readFuture = new ReadFuture(node, new ReadFuture.CompletionCondition() {
@Override
public boolean isComplete(List<EventRecord> events) {
Object latestValue = Iterables.getLast(events).getSnapshot().getValue();
if (events.size() == 1) {
assertEquals("temp value", latestValue);
} else if (events.size() == 2) {
assertEquals(42L, latestValue);
} else {
fail("An extra event was detected");
}
Object val = Iterables.getLast(events).getSnapshot().getValue();
return val != null && count.incrementAndGet() == 2;
}
});
node.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() == null) {
try {
currentData.setValue("temp value");
} catch (DatabaseException e) {
fail("Exception thrown: " + e.toString());
}
return Transaction.success(currentData);
} else {
return Transaction.abort();
}
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertFalse(committed);
assertEquals(42L, currentData.getValue());
}
});
node.getDatabase().goOnline();
List<EventRecord> events = readFuture.timedGet();
Object result = events.get(0).getSnapshot().getValue();
assertEquals("temp value", result);
result = events.get(1).getSnapshot().getValue();
assertEquals(42L, result);
}
@Test
public void testTransactionCreateNode() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final AtomicInteger events = new AtomicInteger(0);
ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
// ignore initial null from the server if we get it.
if (!(events.get() == 0 && snapshot.getValue() == null)) {
events.incrementAndGet();
}
}
@Override
public void onCancelled(DatabaseError error) {
fail("Should not be cancelled");
}
});
final Semaphore semaphore = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
try {
currentData.setValue(42);
} catch (DatabaseException e) {
fail("Should not fail");
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
if (error != null || !committed) {
fail("Transaction should succeed");
} else {
assertEquals(42L, currentData.getValue());
semaphore.release(1);
}
}
});
TestHelpers.waitFor(semaphore);
assertEquals(1, events.get());
}
@Test
public void testTransactionUpdateExistingChildNodes()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
DatabaseReference writer = refs.get(0);
DatabaseReference reader = refs.get(1);
EventHelper helper = new EventHelper().addValueExpectation(reader.child("a"))
.addValueExpectation(reader.child("b")).startListening(true);
writer.child("a").setValue(42);
new WriteFuture(writer.child("b"), 42).timedGet();
assertTrue(helper.waitForEvents());
helper.addValueExpectation(reader.child("b")).startListening();
final Semaphore semaphore = new Semaphore(0);
reader.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
try {
currentData.child("a").setValue(42);
currentData.child("b").setValue(87);
return Transaction.success(currentData);
} catch (DatabaseException e) {
fail("Should not throw");
return Transaction.abort();
}
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
Map expected = new MapBuilder().put("a", 42L).put("b", 87L).build();
assertEquals(expected, currentData.getValue());
semaphore.release(1);
}
});
assertTrue(helper.waitForEvents());
TestHelpers.waitFor(semaphore);
helper.cleanup();
}
@Test
public void testTransactionIsOnlyCalledOnceWhenInitializingAnEmptyNode() throws
InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final AtomicInteger called = new AtomicInteger(0);
final Semaphore semaphore = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
called.incrementAndGet();
assertNull(currentData.getValue());
try {
currentData.child("a").setValue(5);
currentData.child("b").setValue(6);
return Transaction.success(currentData);
} catch (DatabaseException e) {
fail("Should not throw");
return Transaction.abort();
}
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
assertEquals(1, called.get());
}
@Test
public void testSecondTransactionRunImmediatelyOnPreviousOutput() throws InterruptedException {
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
DatabaseReference ref = refs.get(0);
final AtomicBoolean firstRun = new AtomicBoolean(false);
final Semaphore first = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
assertTrue(firstRun.compareAndSet(false, true));
try {
currentData.setValue(42);
return Transaction.success(currentData);
} catch (DatabaseException e) {
fail("Should not throw");
return Transaction.abort();
}
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
first.release(1);
}
});
final AtomicBoolean secondRun = new AtomicBoolean(false);
final Semaphore second = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
assertTrue(secondRun.compareAndSet(false, true));
try {
currentData.setValue(84);
return Transaction.success(currentData);
} catch (DatabaseException e) {
fail("Should not throw");
return Transaction.abort();
}
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
second.release(1);
}
});
TestHelpers.waitFor(first);
TestHelpers.waitFor(second);
DataSnapshot snap = TestHelpers.getSnap(refs.get(1));
assertEquals(84L, snap.getValue());
}
@Test
public void testSetCancelsPendingTransactions()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
// We do 3 transactions: 1) At /foo, 2) At /, and 3) At /bar.
// Only #1 is sent to the server immediately (since 2 depends on 1 and 3
// depends on 2).
// We set /foo to 0.
// - Transaction #1 should complete as planned (since it was already sent).
// - Transaction #2 should be aborted by the set. We keep it from completing
// by hijacking
// the
// hash
// - Transaction #3 should be re-run after #2 is reverted, and then be sent
// to the server
// and
// succeed.
final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
final List<DataSnapshot> nodeSnaps = new ArrayList<>();
final AtomicBoolean firstDone = new AtomicBoolean(false);
final AtomicBoolean secondDone = new AtomicBoolean(false);
final AtomicInteger thirdRunCount = new AtomicInteger(0);
ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
nodeSnaps.add(snapshot);
if (nodeSnaps.size() == 1) {
// we got the initial data
semaphore.release(1);
}
}
@Override
public void onCancelled(DatabaseError error) {
fail("Should not be cancelled");
}
});
ref.child("foo").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
}
@Override
public void onCancelled(DatabaseError error) {
fail("Should not be cancelled");
}
});
TestHelpers.waitFor(semaphore);
final AtomicBoolean firstRun = new AtomicBoolean(false);
ref.child("foo").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
assertTrue(firstRun.compareAndSet(false, true));
currentData.setValue(42);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {
assertTrue(committed);
assertEquals(42L, snapshot.getValue());
firstDone.set(true);
}
});
final AtomicBoolean secondRun = new AtomicBoolean(false);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
ref.getRepo().setHijackHash(true);
assertTrue(secondRun.compareAndSet(false, true));
currentData.child("foo").setValue(84);
currentData.child("bar").setValue(1);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {
ref.getRepo().setHijackHash(false);
assertFalse(committed);
secondDone.set(true);
}
});
ref.child("bar").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
int count = thirdRunCount.incrementAndGet();
if (count == 1) {
assertEquals(1L, currentData.getValue());
currentData.setValue("first");
return Transaction.success(currentData);
} else {
// NOTE: This may get hit more than once because the previous
// transaction
// may still be hijacking transaction hashes.
assertNull(currentData.getValue());
currentData.setValue("second");
return Transaction.success(currentData);
}
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {
assertEquals(null, error);
assertTrue(committed);
assertEquals("second", snapshot.getValue());
semaphore.release();
}
});
// This rolls back the second transaction, and triggers a re-run of the
// third.
// However, a new value event won't be triggered until the listener is
// complete,
// so we're left with the last value event
ref.child("foo").setValue(0);
TestHelpers.waitFor(semaphore);
assertTrue(firstDone.get());
assertTrue(secondDone.get());
// Note that the set actually raises two events, one overlaid on top of the
// original
// transaction
// value, and a second one with the re-run value from the third transaction
}
@Test
public void testTransactionSet() throws InterruptedException {
final Semaphore semaphore = new Semaphore(0);
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
assertNull(currentData.getValue());
currentData.setValue("hi!");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
semaphore.release(1);
}
});
ref.setValue("foo");
ref.setValue("bar");
TestHelpers.waitFor(semaphore);
}
@Test
public void testPriorityNotPreservedWhenSettingData() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
final List<DataSnapshot> snaps = new ArrayList<>();
ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
snaps.add(snapshot);
}
@Override
public void onCancelled(DatabaseError error) {
fail("Should not be cancelled");
}
});
ref.setValue("test", 5);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue("new value");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
assertEquals(2, snaps.size());
assertNull(snaps.get(1).getPriority());
}
// Note: skipping test with nested transactions
@Test
public void testResultingSnapshotIsPassedToOnComplete() throws InterruptedException {
final Semaphore semaphore = new Semaphore(0);
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
DatabaseReference ref1 = refs.get(0);
DatabaseReference ref2 = refs.get(1);
// Add an event listener at this node so we hang on to local state
// in-between transaction
// runs
ref1.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
}
@Override
public void onCancelled(DatabaseError error) {
}
});
ref1.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue("hello!");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals("hello!", currentData.getValue());
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
// Do it again for the aborted case
ref1.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
return Transaction.abort();
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertFalse(committed);
assertEquals("hello!", currentData.getValue());
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
// Now on a fresh connection...
ref2.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() == null) {
currentData.setValue("hello!");
return Transaction.success(currentData);
}
return Transaction.abort();
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertFalse(committed);
assertEquals("hello!", currentData.getValue());
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testTransactionsAbortAfter25Retries() throws InterruptedException {
final Semaphore semaphore = new Semaphore(0);
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
TestHelpers.setHijackHash(ref, true);
final AtomicInteger retries = new AtomicInteger(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
int tries = retries.getAndIncrement();
assertTrue(tries < 25);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNotNull(error);
assertEquals(DatabaseError.MAX_RETRIES, error.getCode());
assertFalse(committed);
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
assertEquals(25, retries.get());
TestHelpers.setHijackHash(ref, false);
}
@Test
public void testSetCancelAlreadySentTransactions()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
final Semaphore semaphore = new Semaphore(0);
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
new WriteFuture(ref, 5).timedGet();
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
assertNull(currentData.getValue());
currentData.setValue(72);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertEquals(DatabaseError.OVERRIDDEN_BY_SET, error.getCode());
assertFalse(committed);
semaphore.release(1);
}
});
ref.setValue(32);
TestHelpers.waitFor(semaphore);
}
@Test
public void testUpdateShouldNotCancelUnrelatedTransactions()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore fooTransaction = new Semaphore(0);
final Semaphore barTransaction = new Semaphore(0);
new WriteFuture(ref.child("foo"), 5).timedGet();
TestHelpers.setHijackHash(ref, true);
// This transaction should get cancelled as we update "foo" later on.
ref.child("foo").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
try {
// Sleep to prevent too many retries due to hash hijacking.
Thread.sleep(10);
} catch (InterruptedException ignore) { // NOLINT
}
currentData.setValue(72);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertEquals(DatabaseError.OVERRIDDEN_BY_SET, error.getCode());
assertFalse(committed);
fooTransaction.release();
}
});
// This transaction should not get cancelled since we don't update "bar".
ref.child("bar").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
try {
// Sleep to prevent too many retries due to hash hijacking.
Thread.sleep(10);
} catch (InterruptedException ignore) { // NOLINT
}
currentData.setValue(72);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
barTransaction.release();
}
});
ref.updateChildren(
fromSingleQuotedString(
"{'foo': 'newValue', 'boo': 'newValue', 'loo' : {'doo' : {'boo': 'newValue'}}}"),
new CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
assertTrue("Should have gotten cancelled before the update",
fooTransaction.availablePermits() > 0);
assertTrue("Should run after the update", barTransaction.availablePermits() == 0);
TestHelpers.setHijackHash(ref, false);
}
});
TestHelpers.waitFor(barTransaction);
}
@Test
public void testTransactionsOnUnicodeData()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
final Semaphore semaphore = new Semaphore(0);
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
new WriteFuture(ref, "♜♞♝♛♚♝♞♜").timedGet();
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() != null) {
assertEquals("♜♞♝♛♚♝♞♜", currentData.getValue());
}
currentData.setValue("♖♘♗♕♔♗♘♖");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals("♖♘♗♕♔♗♘♖", currentData.getValue());
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testImmediatelyAbortingTransaction() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
return Transaction.abort();
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertFalse(committed);
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testAddToAnArrayWithTransaction()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
final Semaphore semaphore = new Semaphore(0);
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
new WriteFuture(ref, Arrays.asList("cat", "horse")).timedGet();
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
Object val = currentData.getValue();
if (val != null) {
@SuppressWarnings("unchecked")
List<String> update = (List<String>) val;
update.add("dog");
currentData.setValue(update);
} else {
currentData.setValue(Collections.singletonList("dog"));
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
ArrayList<String> expected = new ArrayList<>(3);
expected.addAll(Arrays.asList("cat", "horse", "dog"));
Object result = currentData.getValue();
TestHelpers.assertDeepEquals(expected, result);
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testMergedTransactionsHaveCorrectSnapshotInOnComplete()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
final Semaphore semaphore = new Semaphore(0);
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final String nodeName = ref.getKey();
new WriteFuture(ref, new MapBuilder().put("a", 0).build()).timedGet();
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
Object val = currentData.getValue();
if (val != null) {
Map<String, Object> expected = new MapBuilder().put("a", 0L).build();
TestHelpers.assertDeepEquals(expected, val);
}
currentData.setValue(new MapBuilder().put("a", 1L).build());
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
assertEquals(nodeName, currentData.getKey());
Object val = currentData.getValue();
// Per new behavior, will include the accepted value of the transaction,
// if
// it was
// successful.
Map<String, Object> expected = new MapBuilder().put("a", 1L).build();
TestHelpers.assertDeepEquals(expected, val);
semaphore.release(1);
}
});
ref.child("a").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
Object val = currentData.getValue();
if (val != null) {
assertEquals(1L, val);
}
currentData.setValue(2);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
assertEquals("a", currentData.getKey());
assertEquals(2L, currentData.getValue());
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore, 2);
}
// Note: skipping tests for reentrant API calls
@Test
public void testPendingTransactionsAreCancelledOnDisconnect()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
final Semaphore semaphore = new Semaphore(0);
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
new WriteFuture(ref, "initial").timedGet();
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue("new");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertFalse(committed);
assertEquals(DatabaseError.DISCONNECTED, error.getCode());
semaphore.release(1);
}
});
DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp);
RepoManager.interrupt(ctx);
RepoManager.resume(ctx);
TestHelpers.waitFor(semaphore);
}
@Test
public void testTransactionWithLocalEvents1() throws InterruptedException {
final Semaphore semaphore = new Semaphore(0);
final Semaphore completeSemaphore = new Semaphore(0);
final List<DataSnapshot> results = new ArrayList<>();
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
results.add(snapshot);
if (results.size() == 1) {
semaphore.release(1);
}
}
@Override
public void onCancelled(DatabaseError error) {
fail("Should not be cancelled");
}
});
TestHelpers.waitFor(semaphore);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue("hello!");
semaphore.release(1);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
try {
TestHelpers.waitFor(semaphore);
assertTrue(committed);
completeSemaphore.release(1);
} catch (InterruptedException e) {
fail("Should not throw");
}
}
}, false);
TestHelpers.waitFor(semaphore);
assertEquals(1, results.size());
assertNull(results.get(0).getValue());
// Let the completion handler run
semaphore.release(1);
TestHelpers.waitFor(completeSemaphore);
assertEquals(2, results.size());
assertEquals("hello!", results.get(1).getValue());
}
@Test
@Ignore
public void testTransactionWithoutLocalEvents2()
throws InterruptedException, TestFailure, ExecutionException, TimeoutException {
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
final DatabaseReference ref1 = refs.get(0);
DatabaseReference ref2 = refs.get(1);
final Semaphore done = new Semaphore(0);
final List<DataSnapshot> events = new ArrayList<>();
TestHelpers.setHijackHash(ref1, true);
ref1.setValue(0, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
done.release();
}
});
TestHelpers.waitFor(done);
ref1.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
events.add(snapshot);
if (events.size() == 1) {
done.release(1);
}
}
@Override
public void onCancelled(DatabaseError error) {
fail("Should not be cancelled");
}
});
TestHelpers.waitFor(done);
final AtomicInteger retries = new AtomicInteger(0);
ref1.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
retries.getAndIncrement();
Object val = currentData.getValue();
if (val.equals(3L)) {
// Will take effect the next time
TestHelpers.setHijackHash(ref1, false);
}
currentData.setValue("txn result");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals("txn result", currentData.getValue());
done.release(1);
}
}, false);
for (int i = 0; i < 4; ++i) {
new WriteFuture(ref2, i).timedGet();
}
TestHelpers.waitFor(done);
assertTrue(retries.get() > 1);
int size = events.size();
assertEquals("txn result", events.get(size - 1).getValue());
// Note: this test doesn't map cleanly, there is some potential for race
// conditions.
// Check that end state is what we want
}
// NOTE: skipping test for reentrant API calls
@Test
public void testTransactionRunsOnNullOnlyOnceAfterReconnectCase1981()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
new WriteFuture(ref, 42).timedGet();
DatabaseReference newRef = FirebaseDatabase.getInstance(masterApp).getReference(ref.getKey());
final Semaphore done = new Semaphore(0);
final AtomicInteger runs = new AtomicInteger(0);
newRef.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
int run = runs.incrementAndGet();
if (run == 1) {
assertNull(currentData.getValue());
} else if (run == 2) {
assertEquals(42L, currentData.getValue());
} else {
fail("Too many calls");
return Transaction.abort();
}
currentData.setValue(3.14);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals(2, runs.get());
assertEquals(3.14, currentData.getValue());
done.release(1);
}
});
TestHelpers.waitFor(done);
}
@Test
public void testTransactionRespectsPriority() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore done = new Semaphore(0);
final List<DataSnapshot> values = new ArrayList<>();
ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
values.add(snapshot);
}
@Override
public void onCancelled(DatabaseError error) {
fail("Should not be cancelled");
}
});
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue(5);
currentData.setPriority(5);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
done.release(1);
}
});
TestHelpers.waitFor(done);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
assertEquals(5L, currentData.getValue());
assertEquals(5.0, currentData.getPriority());
currentData.setValue(10);
currentData.setPriority(10);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
done.release(1);
}
});
TestHelpers.waitFor(done);
assertEquals(5L, values.get(values.size() - 2).getValue());
assertEquals(5.0, values.get(values.size() - 2).getPriority());
assertEquals(10L, values.get(values.size() - 1).getValue());
assertEquals(10.0, values.get(values.size() - 1).getPriority());
}
@Test
public void testTransactionRevertsDataWhenAddingDeeperListen() throws InterruptedException {
final List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
final Semaphore gotTest = new Semaphore(0);
refs.get(0).child("y").setValue("test", new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
refs.get(1).runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() == null) {
currentData.child("x").setValue(5);
return Transaction.success(currentData);
} else {
return Transaction.abort();
}
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
}
});
refs.get(1).child("y").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
if ("test".equals(snapshot.getValue())) {
gotTest.release(1);
}
}
@Override
public void onCancelled(DatabaseError error) {
}
});
}
});
TestHelpers.waitFor(gotTest);
}
@Test
public void testTransactionWithNumericKeys() throws InterruptedException {
final DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore done = new Semaphore(0);
Map<String, Object> initial = new MapBuilder().put("1", 1L).put("5", 5L).put("10", 5L)
.put("20", 20L).build();
ref.setValue(initial, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue(42);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
assertNull(error);
done.release(1);
}
});
}
});
TestHelpers.waitFor(done);
}
@Test
public void testRemoveChildWithPriority()
throws InterruptedException, ExecutionException, TimeoutException, TestFailure {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore done = new Semaphore(0);
long value = 1378744239756L;
new WriteFuture(ref, value, 0).timedGet();
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue(null);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertTrue(committed);
assertNull(currentData.getValue());
assertNull(error);
done.release(1);
}
});
TestHelpers.waitFor(done);
}
@Test
public void testUserCodeExceptionsAbortTheTransaction() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore done = new Semaphore(0);
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
throw new NullPointerException("lol! user code!");
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertFalse(committed);
assertEquals(DatabaseError.USER_CODE_EXCEPTION, error.getCode());
done.release(1);
}
});
TestHelpers.waitFor(done);
// Now try it with a Throwable, rather than exception
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
throw new StackOverflowError();
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertFalse(committed);
assertEquals(DatabaseError.USER_CODE_EXCEPTION, error.getCode());
done.release(1);
}
});
TestHelpers.waitFor(done);
}
@Test
public void testBubbleAppTransactionBug() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore done = new Semaphore(0);
ref.child("a").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue(1);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
}
});
ref.child("a").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() != null) {
currentData.setValue((Long) currentData.getValue() + 42);
} else {
currentData.setValue(42);
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
}
});
ref.child("b").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue(7);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
}
});
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() != null) {
currentData.setValue(
(Long) currentData.child("a").getValue() + (Long) currentData.child("b").getValue());
} else {
currentData.setValue("dummy");
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals(50L, currentData.getValue());
done.release(1);
}
});
TestHelpers.waitFor(done);
}
@Test
public void testLocalServerValuesEventuallyButNotImmediatelyMatchServer()
throws InterruptedException {
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
DatabaseReference writer = refs.get(0);
DatabaseReference reader = refs.get(1);
final Semaphore completionSemaphore = new Semaphore(0);
final List<DataSnapshot> readSnaps = new ArrayList<>();
final List<DataSnapshot> writeSnaps = new ArrayList<>();
reader.addValueEventListener(new ValueEventListener() {
@Override
public void onCancelled(DatabaseError error) {
}
@Override
public void onDataChange(DataSnapshot snapshot) {
if (snapshot.getValue() != null) {
readSnaps.add(snapshot);
completionSemaphore.release();
}
}
});
writer.addValueEventListener(new ValueEventListener() {
@Override
public void onCancelled(DatabaseError error) {
}
@Override
public void onDataChange(DataSnapshot snapshot) {
if (snapshot.getValue() != null) {
writeSnaps.add(snapshot);
completionSemaphore.release();
}
}
});
// Go offline for a few ms to make sure we get a different timestamp than
// the server.
writer.getDatabase().goOffline();
writer.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue(ServerValue.TIMESTAMP);
currentData.setPriority(ServerValue.TIMESTAMP);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
if (error != null || !committed) {
fail("Transaction should succeed");
}
}
});
Thread.sleep(5);
writer.getDatabase().goOnline();
TestHelpers.waitFor(completionSemaphore, 4);
assertEquals(2, readSnaps.size());
assertEquals(2, writeSnaps.size());
assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(0).getValue()) < 6000);
assertTrue(
Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(0).getPriority()) < 6000);
assertTrue(Math.abs(System.currentTimeMillis() - (Long) writeSnaps.get(1).getValue()) < 6000);
assertTrue(
Math.abs(System.currentTimeMillis() - (Double) writeSnaps.get(1).getPriority()) < 6000);
assertFalse(writeSnaps.get(0).getValue().equals(writeSnaps.get(1).getValue()));
assertFalse(writeSnaps.get(0).getPriority().equals(writeSnaps.get(1).getPriority()));
assertEquals(writeSnaps.get(1).getValue(), readSnaps.get(1).getValue());
assertEquals(writeSnaps.get(1).getPriority(), readSnaps.get(1).getPriority());
}
@Test
public void testTransactionWithQueryListen() throws InterruptedException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
ref.setValue(new MapBuilder().put("a", 1).put("b", 2).build(),
new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
ref.limitToFirst(1).addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
}
@Override
public void onChildChanged(DataSnapshot snapshot, String previousChildName) {
}
@Override
public void onChildRemoved(DataSnapshot snapshot) {
}
@Override
public void onChildMoved(DataSnapshot snapshot, String previousChildName) {
}
@Override
public void onCancelled(DatabaseError error) {
}
});
ref.child("a").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed,
DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals(1L, currentData.getValue());
semaphore.release();
}
});
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testTransactionDoesNotPickUpCachedDataFromPrevious() throws InterruptedException {
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
final DatabaseReference me = refs.get(0);
final DatabaseReference other = refs.get(1);
final Semaphore semaphore = new Semaphore(0);
me.setValue("not null", new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
semaphore.release();
}
});
TestHelpers.waitFor(semaphore);
me.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
semaphore.release();
}
@Override
public void onCancelled(DatabaseError error) {
}
});
TestHelpers.waitFor(semaphore);
other.setValue(null, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
semaphore.release();
}
});
TestHelpers.waitFor(semaphore);
me.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() == null) {
currentData.setValue("it was null!");
} else {
currentData.setValue("it was not null!");
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals("it was null!", currentData.getValue());
semaphore.release();
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testTransactionDoesNotPickUpCachedDataFromPreviousTransaction()
throws InterruptedException {
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
final DatabaseReference me = refs.get(0);
final DatabaseReference other = refs.get(1);
final Semaphore semaphore = new Semaphore(0);
me.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue("not null");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
semaphore.release();
}
});
TestHelpers.waitFor(semaphore);
other.setValue(null, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
semaphore.release();
}
});
TestHelpers.waitFor(semaphore);
me.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
if (currentData.getValue() == null) {
currentData.setValue("it was null!");
} else {
currentData.setValue("it was not null!");
}
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
assertEquals("it was null!", currentData.getValue());
semaphore.release();
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testTransactionOnQueriedLocationDoesNotRunInitiallyOnNull()
throws InterruptedException, ExecutionException, TimeoutException, TestFailure {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
DatabaseReference child = ref.push();
final Map<String, Object> initialData = new MapBuilder().put("a", 1L).put("b", 2L).build();
new WriteFuture(child, initialData).timedGet();
final Semaphore semaphore = new Semaphore(0);
ChildEventListener listener = ref.limitToFirst(1)
.addChildEventListener(new TestChildEventListener() {
@Override
public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
snapshot.getRef().runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
Assert.assertEquals(initialData, currentData.getValue());
currentData.setValue(null);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed,
DataSnapshot currentData) {
Assert.assertNull(error);
Assert.assertTrue(committed);
Assert.assertNull(currentData.getValue());
semaphore.release();
}
});
}
@Override
public void onChildRemoved(DataSnapshot snapshot) {
}
});
TestHelpers.waitFor(semaphore);
// cleanup
ref.removeEventListener(listener);
}
@Test
public void testTransactionsRaiseCorrectChildChangedEventsOnQueries()
throws InterruptedException, ExecutionException, TimeoutException, TestFailure {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
Map<String, Object> value = new MapBuilder()
.put("foo", new MapBuilder().put("value", 1).build()).build();
new WriteFuture(ref, value).timedGet();
final List<DataSnapshot> snapshots = new ArrayList<>();
ChildEventListener listener = new TestChildEventListener() {
@Override
public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
snapshots.add(snapshot);
}
@Override
public void onChildChanged(DataSnapshot snapshot, String previousChildName) {
snapshots.add(snapshot);
}
};
ref.endAt(Double.MIN_VALUE).addChildEventListener(listener);
final Semaphore semaphore = new Semaphore(0);
ref.child("foo").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.child("value").setValue(2);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
semaphore.release();
}
}, false);
TestHelpers.waitFor(semaphore);
Assert.assertEquals(2, snapshots.size());
DataSnapshot addedSnapshot = snapshots.get(0);
Assert.assertEquals("foo", addedSnapshot.getKey());
Assert.assertEquals(new MapBuilder().put("value", 1L).build(), addedSnapshot.getValue());
DataSnapshot changedSnapshot = snapshots.get(1);
Assert.assertEquals("foo", changedSnapshot.getKey());
Assert.assertEquals(new MapBuilder().put("value", 2L).build(), changedSnapshot.getValue());
// cleanup
ref.removeEventListener(listener);
}
@Test
public void testTransactionsUseLocalMerges()
throws InterruptedException, ExecutionException, TimeoutException, TestFailure {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
// Go offline to ensure the update doesn't complete before the transaction
// runs.
DatabaseConfig ctx = TestHelpers.getDatabaseConfig(masterApp);
RepoManager.interrupt(ctx);
ref.updateChildren(new MapBuilder().put("foo", "bar").build());
ref.child("foo").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
assertEquals("bar", currentData.getValue());
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertEquals(null, error);
assertTrue(committed);
semaphore.release();
}
});
RepoManager.resume(ctx);
TestHelpers.waitFor(semaphore);
}
@Test
public void testOutOfOrderRemoveWrites()
throws InterruptedException, ExecutionException, TestFailure, TimeoutException {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
ref.setValue(new MapBuilder().put("foo", "bar").build());
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue("transaction-1");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
}
});
ref.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue("transaction-2");
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
}
});
final Semaphore done = new Semaphore(0);
// This will trigger an abort of the transaction which should not cause the
// client to crash
ref.updateChildren(new MapBuilder().put("qux", "quu").build(),
new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
done.release();
}
});
TestHelpers.waitFor(done);
}
@Test
public void testReturningNullReturnsNullPointerException() throws Throwable {
DatabaseReference ref = IntegrationTestUtils.getRandomNode(masterApp);
final Semaphore semaphore = new Semaphore(0);
ref.child("foo").runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
return null;
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNotNull(error);
semaphore.release();
}
});
TestHelpers.waitFor(semaphore);
}
@Test
public void testUnsentTransactionsAreNotCancelledOnDisconnect() throws InterruptedException {
// Hack: To trigger us to disconnect before restoring state, we inject a bad
// auth token.
// In real-world usage the much more common case is that we get redirected
// to a different
// server, but that's harder to manufacture from a test.
final DatabaseConfig cfg = TestHelpers.getDatabaseConfig(masterApp);
cfg.setAuthTokenProvider(new AuthTokenProvider() {
private int count = 0;
@Override
public void getToken(boolean forceRefresh, final GetTokenCompletionListener listener) {
// Return "bad-token" once to trigger a disconnect, and then a null
// token.
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError = TestHelpers.getExecutorService(cfg)
.schedule(new Runnable() {
@Override
public void run() {
if (count == 0) {
count++;
listener.onSuccess("bad-token");
} else {
listener.onSuccess(null);
}
}
}, 10, TimeUnit.MILLISECONDS);
}
@Override
public void addTokenChangeListener(TokenChangeListener listener) {
}
@Override
public void removeTokenChangeListener(TokenChangeListener listener) {
}
});
// Queue a transaction offline.
DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference();
ref.getDatabase().goOffline();
final Semaphore semaphore = new Semaphore(0);
ref.push().runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData currentData) {
currentData.setValue(42);
return Transaction.success(currentData);
}
@Override
public void onComplete(DatabaseError error, boolean committed, DataSnapshot currentData) {
assertNull(error);
assertTrue(committed);
semaphore.release();
}
});
ref.getDatabase().goOnline();
TestHelpers.waitFor(semaphore);
}
}