/*
* 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 org.junit.Assert.assertEquals;
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.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.TestOnlyImplFirebaseTrampolines;
import com.google.firebase.auth.FirebaseCredentials;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.EventRecord;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.MapBuilder;
import com.google.firebase.database.TestFailure;
import com.google.firebase.database.TestHelpers;
import com.google.firebase.database.TestTokenProvider;
import com.google.firebase.database.ValueEventListener;
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.database.util.JsonMapper;
import com.google.firebase.tasks.Tasks;
import com.google.firebase.testing.IntegrationTestUtils;
import com.google.firebase.testing.TestUtils;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
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 java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class RulesTestIT {
private static final Map<String, Object> DEFAULT_RULES_STRING =
MapBuilder.of("rules", MapBuilder.of(".read", "auth != null", ".write", "auth != null"));
private static final Map<String, Object> testRules;
static {
testRules = new MapBuilder()
.put("read_only", MapBuilder.of(".read", true))
.put("write_only", MapBuilder.of(".write", true))
.put("read_and_write",
new MapBuilder().put(".write", true)
.put(".read",
true)
.build())
.put("any_auth",
new MapBuilder().put(".write", "auth != null").put(".read", "auth != null").build())
.put("revocable", new MapBuilder().put(".write", true)
.put(".read",
"data.child('public').val() == true && data.child('hidden').val() != true")
.build())
.put("users", new MapBuilder().put(".write", true).put(".read", true)
.put("$user", new MapBuilder().put(".validate", "newData.hasChildren(['name', 'age'])")
.put("name",
new MapBuilder().put(".validate", "newData.isString() == true").build())
.put("age",
new MapBuilder()
.put(".validate", "newData.isNumber() && newData.val() > " + "13").build())
.build())
.build())
.build();
}
private static DatabaseReference reader;
private static DatabaseReference writer;
private static FirebaseApp masterApp;
@BeforeClass
public static void setUpClass() throws IOException {
// Init app with non-admin privileges
Map<String, Object> auth = MapBuilder.of("uid", "my-service-worker");
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredential(FirebaseCredentials
.fromCertificate(IntegrationTestUtils.getServiceAccountCertificate()))
.setDatabaseUrl(IntegrationTestUtils.getDatabaseUrl())
.setDatabaseAuthVariableOverride(auth)
.build();
masterApp = FirebaseApp.initializeApp(options, "RulesTestIT");
List<DatabaseReference> refs = IntegrationTestUtils.getRandomNode(masterApp, 2);
reader = refs.get(0);
writer = refs.get(1);
String rules = JsonMapper.serializeJson(
MapBuilder.of("rules", MapBuilder.of(writer.getKey(), testRules)));
uploadRules(rules);
TestHelpers.waitForRoundtrip(writer.getRoot());
}
@AfterClass
public static void tearDownClass() throws IOException {
uploadRules(JsonMapper.serializeJson(DEFAULT_RULES_STRING));
TestHelpers.waitForRoundtrip(writer.getRoot());
}
@Before
public void prepareApp() {
TestHelpers.wrapForErrorHandling(masterApp);
}
@After
public void checkAndCleanupApp() {
TestHelpers.assertAndUnwrapErrorHandlers(masterApp);
}
private static void uploadRules(String rules) throws IOException {
IntegrationTestUtils.AppHttpClient client = new IntegrationTestUtils.AppHttpClient(masterApp);
IntegrationTestUtils.ResponseInfo response = client.put("/.settings/rules.json", rules);
assertEquals(200, response.getStatus());
}
@Test
public void testWriteOperationSetsErrorOnFailure() throws InterruptedException {
DatabaseReference ref = writer.child("read_only");
final Semaphore semaphore = new Semaphore(0);
final AtomicReference<DatabaseError> result = new AtomicReference<>();
ref.setValue("value", new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
result.compareAndSet(null, error);
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
assertNotNull(result.get());
// All of the other writing methods are just testing the server. We're just
// testing the
// error propagation.
}
@Test
public void testFailedListensDoNotDisruptOtherListens()
throws TestFailure, TimeoutException, InterruptedException {
// Wait until we're connected
ReadFuture.untilEquals(reader.getRoot().child(".info/connected"), true).timedGet();
final Semaphore semaphore = new Semaphore(0);
reader.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
fail("Should not get data");
}
@Override
public void onCancelled(DatabaseError error) {
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
final AtomicBoolean saw42 = new AtomicBoolean(false);
final ValueEventListener listener = reader.child("read_and_write")
.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
// No-op
Integer value = snapshot.getValue(Integer.class);
if (value != null) {
if (value == 42) {
assertTrue(saw42.compareAndSet(false, true));
semaphore.release(1);
} else if (value == 84) {
assertTrue(saw42.get());
semaphore.release(1);
} else {
fail("unexpected value");
}
}
}
@Override
public void onCancelled(DatabaseError error) {
fail("This one shouldn't fail");
}
});
writer.child("read_and_write").setValue(42);
TestHelpers.waitFor(semaphore);
writer.child("read_and_write").setValue(84);
TestHelpers.waitFor(semaphore);
reader.child("read_and_write").removeEventListener(listener);
}
@Test
public void testFailedListensDoNotDisruptOtherListens2()
throws TestFailure, TimeoutException, InterruptedException {
// Wait until we're connected
ReadFuture.untilEquals(reader.getRoot().child(".info/connected"), true).timedGet();
final Semaphore semaphore = new Semaphore(0);
final AtomicBoolean saw42 = new AtomicBoolean(false);
final ValueEventListener listener = reader.child("read_and_write")
.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
// No-op
Integer value = snapshot.getValue(Integer.class);
if (value != null) {
if (value == 42) {
assertTrue(saw42.compareAndSet(false, true));
semaphore.release(1);
} else if (value == 84) {
assertTrue(saw42.get());
semaphore.release(1);
} else {
fail("unexpected value");
}
}
}
@Override
public void onCancelled(DatabaseError error) {
fail("This one shouldn't fail");
}
});
writer.child("read_and_write").setValue(42);
TestHelpers.waitFor(semaphore);
reader.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
fail("Should not get data");
}
@Override
public void onCancelled(DatabaseError error) {
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
writer.child("read_and_write").setValue(84);
TestHelpers.waitFor(semaphore);
reader.child("read_and_write").removeEventListener(listener);
}
@Test
public void testListenRevocation()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
new WriteFuture(writer.child("revocable"),
new MapBuilder().put("public", true).put("data", 1).build()).timedGet();
final AtomicBoolean valueHit = new AtomicBoolean(false);
final AtomicInteger value = new AtomicInteger(0);
final Semaphore semaphore = new Semaphore(0);
reader.child("revocable/data").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
valueHit.compareAndSet(false, true);
value.compareAndSet(0, snapshot.getValue(Integer.class));
semaphore.release(1);
}
@Override
public void onCancelled(DatabaseError error) {
semaphore.release(1);
}
});
TestHelpers.waitFor(semaphore);
assertTrue(valueHit.get());
assertEquals(1, value.get());
writer.child("revocable/public").setValue(false);
writer.child("revocable/data").setValue(2);
TestHelpers.waitFor(semaphore);
assertTrue(valueHit.get());
writer.child("revocable/public").setValue(true);
new WriteFuture(writer.child("revocable/data"), 3).timedGet();
// Ok, the listen was cancelled, create a new one now.
ValueEventListener listener = reader.child("revocable/data")
.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
assertEquals(3, (int) snapshot.getValue(Integer.class));
semaphore.release(1);
}
@Override
public void onCancelled(DatabaseError error) {
fail("This listen shouldn't fail");
}
});
TestHelpers.waitFor(semaphore);
reader.child("revocable/data").removeEventListener(listener);
}
@Test
public void testFailedSetsRolledBack()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
DatabaseReference fredRef = reader.child("users/fred");
new WriteFuture(fredRef, new MapBuilder().put("name", "Fred").put("age", 19).build())
.timedGet();
final Semaphore semaphore = new Semaphore(0);
ReadFuture future = new ReadFuture(fredRef.child("age"), new ReadFuture.CompletionCondition() {
@Override
public boolean isComplete(List<EventRecord> events) {
if (events.size() == 1) {
semaphore.release(1);
}
return events.size() == 3;
}
});
// Wait for initial data
TestHelpers.waitFor(semaphore);
fredRef.child("age").setValue(12, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
assertNotNull(error);
semaphore.release(1);
}
});
List<EventRecord> events = future.timedGet();
assertEquals(19, (int) events.get(0).getSnapshot().getValue(Integer.class));
assertEquals(12, (int) events.get(1).getSnapshot().getValue(Integer.class));
assertEquals(19, (int) events.get(2).getSnapshot().getValue(Integer.class));
TestHelpers.waitFor(semaphore);
}
@Test
public void testFailedUpdatesRolledBack()
throws TestFailure, ExecutionException, TimeoutException, InterruptedException {
DatabaseReference fredRef = reader.child("users/fred");
new WriteFuture(fredRef, new MapBuilder().put("name", "Fred").put("age", 19).build())
.timedGet();
final Semaphore semaphore = new Semaphore(0);
ReadFuture future = new ReadFuture(fredRef.child("age"), new ReadFuture.CompletionCondition() {
@Override
public boolean isComplete(List<EventRecord> events) {
if (events.size() == 1) {
semaphore.release(1);
}
return events.size() == 3;
}
});
// Wait for initial data
TestHelpers.waitFor(semaphore);
Map<String, Object> update = MapBuilder.of("age", 12);
fredRef.updateChildren(update, new DatabaseReference.CompletionListener() {
@Override
public void onComplete(DatabaseError error, DatabaseReference ref) {
assertNotNull(error);
semaphore.release(1);
}
});
List<EventRecord> events = future.timedGet();
assertEquals(19, (int) events.get(0).getSnapshot().getValue(Integer.class));
assertEquals(12, (int) events.get(1).getSnapshot().getValue(Integer.class));
assertEquals(19, (int) events.get(2).getSnapshot().getValue(Integer.class));
TestHelpers.waitFor(semaphore);
}
@Test
public void testStillAuthenticatedAfterReconnect()
throws InterruptedException, ExecutionException, TestFailure, TimeoutException {
DatabaseConfig config = TestHelpers.getDatabaseConfig(masterApp);
DatabaseReference root = FirebaseDatabase.getInstance(masterApp).getReference();
DatabaseReference ref = root.child(writer.getPath().toString());
RepoManager.interrupt(config);
RepoManager.resume(config);
DatabaseError err = new WriteFuture(ref.child("any_auth"), true).timedGet();
assertNull(err);
}
@Test
public void testAuthenticatedImmediatelyAfterTokenChange() throws Exception {
DatabaseConfig config = TestHelpers.getDatabaseConfig(masterApp);
TestTokenProvider provider = new TestTokenProvider(TestHelpers.getExecutorService(config));
config.setAuthTokenProvider(provider);
DatabaseReference root = FirebaseDatabase.getInstance(masterApp).getReference();
DatabaseReference ref = root.child(writer.getPath().toString());
String token = Tasks.await(TestOnlyImplFirebaseTrampolines.getToken(masterApp, true),
TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS).getToken();
provider.setToken(token);
DatabaseError err = new WriteFuture(ref.child("any_auth"), true).timedGet();
assertNull(err);
}
}