diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java index 2dd21ee50..956167840 100644 --- a/src/main/java/com/google/firebase/FirebaseApp.java +++ b/src/main/java/com/google/firebase/FirebaseApp.java @@ -2,19 +2,19 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; -import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; import com.google.firebase.internal.AuthStateListener; import com.google.firebase.internal.FirebaseAppStore; import com.google.firebase.internal.FirebaseExecutors; +import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.GetTokenResult; -import com.google.firebase.internal.GuardedBy; import com.google.firebase.internal.Joiner; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; @@ -29,7 +29,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -37,10 +36,10 @@ /** * The entry point of Firebase SDKs. It holds common configuration and state for Firebase APIs. Most - * applications don't need to directly interact with FirebaseApp. * + * applications don't need to directly interact with FirebaseApp. * *

Firebase APIs use the default FirebaseApp by default, unless a different one is explicitly - * passed to the API via FirebaseFoo.getInstance(firebaseApp). * + * passed to the API via FirebaseFoo.getInstance(firebaseApp). * *

{@link FirebaseApp#initializeApp(FirebaseOptions)} initializes the default app instance. This * method should be invoked at startup. @@ -48,27 +47,32 @@ public class FirebaseApp { /** A map of (name, FirebaseApp) instances. */ - @GuardedBy("sLock") private static final Map instances = new HashMap<>(); public static final String DEFAULT_APP_NAME = "[DEFAULT]"; private static final long TOKEN_REFRESH_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(55); private static final TokenRefresher.Factory DEFAULT_TOKEN_REFRESHER_FACTORY = new TokenRefresher.Factory(); - private static final Object sLock = new Object(); + + /** + * Global lock for synchronizing all SDK-wide application state changes. Specifically, any + * accesses to instances map should be protected by this lock. + */ + private static final Object appsLock = new Object(); private final String name; private final FirebaseOptions options; private final TokenRefresher tokenRefresher; private final AtomicBoolean deleted = new AtomicBoolean(); - - private final List lifecycleListeners = - new CopyOnWriteArrayList<>(); - private final List authStateListeners = new ArrayList<>(); - private final AtomicReference currentToken = new AtomicReference<>(); + private final Map services = new HashMap<>(); + + /** + * Per application lock for synchronizing all internal FirebaseApp state changes. + */ + private final Object lock = new Object(); /** Default constructor. */ private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory factory) { @@ -81,7 +85,9 @@ private FirebaseApp(String name, FirebaseOptions options, TokenRefresher.Factory /** Returns a mutable list of all FirebaseApps. */ public static List getApps() { // TODO(arondeak): reenable persistence. See b/28158809. - return new ArrayList<>(instances.values()); + synchronized (appsLock) { + return ImmutableList.copyOf(instances.values()); + } } /** @@ -103,7 +109,7 @@ public static FirebaseApp getInstance() { * #initializeApp(FirebaseOptions, String)} or {@link #getApps()}. */ public static FirebaseApp getInstance(@NonNull String name) { - synchronized (sLock) { + synchronized (appsLock) { FirebaseApp firebaseApp = instances.get(normalize(name)); if (firebaseApp != null) { return firebaseApp; @@ -154,8 +160,8 @@ static FirebaseApp initializeApp( FirebaseAppStore appStore = FirebaseAppStore.initialize(); String normalizedName = normalize(name); final FirebaseApp firebaseApp; - synchronized (sLock) { - Preconditions.checkState( + synchronized (appsLock) { + checkState( !instances.containsKey(normalizedName), "FirebaseApp name " + normalizedName + " already exists!"); @@ -170,8 +176,12 @@ static FirebaseApp initializeApp( @VisibleForTesting static void clearInstancesForTest() { - // TODO(arondeak): also delete, once functionality is implemented. - synchronized (sLock) { + synchronized (appsLock) { + // Copy the instances list before iterating, as delete() would attempt to remove from the + // original list. + for (FirebaseApp app : ImmutableList.copyOf(instances.values())) { + app.delete(); + } instances.clear(); } } @@ -191,7 +201,7 @@ String getPersistenceKey() { private static List getAllAppNames() { Set allAppNames = new HashSet<>(); - synchronized (sLock) { + synchronized (appsLock) { for (FirebaseApp app : instances.values()) { allAppNames.add(app.getName()); } @@ -250,27 +260,33 @@ public String toString() { * *

A no-op if delete was called before. */ - void delete() { - boolean valueChanged = deleted.compareAndSet(false /* expected */, true); - if (!valueChanged) { - return; + public void delete() { + synchronized (lock) { + boolean valueChanged = deleted.compareAndSet(false /* expected */, true); + if (!valueChanged) { + return; + } + + for (FirebaseService service : services.values()) { + service.destroy(); + } + services.clear(); + authStateListeners.clear(); + tokenRefresher.cleanup(); } - tokenRefresher.cleanup(); - synchronized (sLock) { - instances.remove(this.name); + synchronized (appsLock) { + instances.remove(name); } FirebaseAppStore appStore = FirebaseAppStore.getInstance(); if (appStore != null) { appStore.removeApp(name); } - - notifyOnAppDeleted(); } private void checkNotDeleted() { - Preconditions.checkState(!deleted.get(), "FirebaseApp was deleted"); + checkState(!deleted.get(), "FirebaseApp was deleted %s", this); } /** @@ -293,10 +309,14 @@ public GetTokenResult then(@NonNull Task task) throws Exception { GetTokenResult oldToken = currentToken.get(); List listenersCopy = null; if (!newToken.equals(oldToken)) { - synchronized (authStateListeners) { + synchronized (lock) { + if (deleted.get()) { + return newToken; + } + // Grab the lock before compareAndSet to avoid a potential race - // condition - // with addAuthStateListener + // condition with addAuthStateListener. The same lock also ensures serial + // access to the token refresher. if (currentToken.compareAndSet(oldToken, newToken)) { listenersCopy = ImmutableList.copyOf(authStateListeners); tokenRefresher.scheduleRefresh(TOKEN_REFRESH_INTERVAL_MILLIS); @@ -318,55 +338,48 @@ boolean isDefaultApp() { return DEFAULT_APP_NAME.equals(getName()); } - /** - * If an API has locally stored data it must register lifecycle listeners at initialization time. - */ - // TODO(arondeak): make sure that all APIs that are interested in these events are - // initialized using reflection when an app is deleted (for v5). - void addLifecycleEventListener(@NonNull FirebaseAppLifecycleListener listener) { - checkNotDeleted(); - lifecycleListeners.add(checkNotNull(listener)); - } - - void removeLifecycleEventListener(@NonNull FirebaseAppLifecycleListener listener) { - checkNotDeleted(); - lifecycleListeners.remove(checkNotNull(listener)); - } - void addAuthStateListener(@NonNull final AuthStateListener listener) { - checkNotDeleted(); - checkNotNull(listener); - GetTokenResult currentToken; - synchronized (authStateListeners) { - authStateListeners.add(listener); + synchronized (lock) { + checkNotDeleted(); + authStateListeners.add(checkNotNull(listener)); currentToken = this.currentToken.get(); } if (currentToken != null) { - // Task has copied the mAuthStateListeners before the listener was added. + // Task has copied the authStateListeners before the listener was added. // Notify this listener explicitly. listener.onAuthStateChanged(currentToken); } } void removeAuthStateListener(@NonNull AuthStateListener listener) { - checkNotDeleted(); - checkNotNull(listener); - synchronized (authStateListeners) { - authStateListeners.remove(listener); + synchronized (lock) { + checkNotDeleted(); + authStateListeners.remove(checkNotNull(listener)); } } - /** - * Notifies all listeners with the name and options of the deleted {@link FirebaseApp} instance. - */ - private void notifyOnAppDeleted() { - for (FirebaseAppLifecycleListener listener : lifecycleListeners) { - listener.onDeleted(name, options); + void addService(FirebaseService service) { + synchronized (lock) { + checkNotDeleted(); + checkArgument(!services.containsKey(checkNotNull(service).getId())); + services.put(service.getId(), service); } } + FirebaseService getService(String id) { + synchronized (lock) { + checkArgument(!Strings.isNullOrEmpty(id)); + return services.get(id); + } + } + + /** + * Utility class for scheduling proactive token refresh events. Each FirebaseApp should have + * its own instance of this class. This class is not thread safe. The caller (FirebaseApp) must + * ensure that methods are called serially. + */ static class TokenRefresher { private final FirebaseApp firebaseApp; @@ -382,7 +395,7 @@ static class TokenRefresher { * @param delayMillis Duration in milliseconds, after which the token should be forcibly * refreshed. */ - final synchronized void scheduleRefresh(long delayMillis) { + final void scheduleRefresh(long delayMillis) { cancelPrevious(); scheduleNext( new Callable>() { @@ -410,14 +423,13 @@ protected void scheduleNext(Callable> task, long delayMilli } } - protected synchronized void cleanup() { + protected void cleanup() { if (future != null) { future.cancel(true); } } static class Factory { - TokenRefresher create(FirebaseApp app) { return new TokenRefresher(app); } diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java index 3761a2b4a..62c22747a 100644 --- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java +++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java @@ -2,6 +2,7 @@ import com.google.firebase.auth.FirebaseCredential; import com.google.firebase.internal.AuthStateListener; +import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.GetTokenResult; import com.google.firebase.internal.NonNull; import com.google.firebase.tasks.Task; @@ -31,16 +32,6 @@ public static String getPersistenceKey(String name, FirebaseOptions options) { return FirebaseApp.getPersistenceKey(name, options); } - public static void addLifecycleEventListener( - @NonNull FirebaseApp app, @NonNull FirebaseAppLifecycleListener listener) { - app.addLifecycleEventListener(listener); - } - - public static void removeLifecycleEventListener( - @NonNull FirebaseApp app, @NonNull FirebaseAppLifecycleListener listener) { - app.removeLifecycleEventListener(listener); - } - public static void addAuthStateChangeListener( @NonNull FirebaseApp app, @NonNull AuthStateListener listener) { app.addAuthStateListener(listener); @@ -54,4 +45,15 @@ public static void removeAuthStateChangeListener( public static Task getToken(@NonNull FirebaseApp app, boolean forceRefresh) { return app.getToken(forceRefresh); } + + public static T getService( + @NonNull FirebaseApp app, @NonNull String id, @NonNull Class type) { + return type.cast(app.getService(id)); + } + + public static T addService( + @NonNull FirebaseApp app, @NonNull T service) { + app.addService(service); + return service; + } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index 1136423cd..f54d43875 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -11,12 +11,12 @@ import com.google.firebase.ImplFirebaseTrampolines; import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.auth.internal.FirebaseTokenVerifier; +import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.NonNull; import com.google.firebase.tasks.Continuation; import com.google.firebase.tasks.Task; import com.google.firebase.tasks.Tasks; -import java.util.HashMap; import java.util.Map; /** @@ -31,11 +31,6 @@ public class FirebaseAuth { /** A global, thread-safe Json Factory built using Gson. */ private static final JsonFactory jsonFactory = new GsonFactory(); - /** - * A static map of FirebaseApp name to FirebaseAuth instance. To ensure thread- safety, it should - * only be accessed in getInstance(), which is a synchronized method. - */ - private static Map authInstances = new HashMap<>(); private final FirebaseApp firebaseApp; private final GooglePublicKeysManager googlePublicKeysManager; @@ -73,11 +68,12 @@ public static FirebaseAuth getInstance() { * @return A FirebaseAuth instance. */ public static synchronized FirebaseAuth getInstance(FirebaseApp app) { - if (!authInstances.containsKey(app.getName())) { - authInstances.put(app.getName(), new FirebaseAuth(app)); + FirebaseAuthService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, + FirebaseAuthService.class); + if (service == null) { + service = ImplFirebaseTrampolines.addService(app, new FirebaseAuthService(app)); } - - return authInstances.get(app.getName()); + return service.getInstance(); } /** @@ -182,4 +178,20 @@ public FirebaseToken then(@NonNull Task task) throws Exception { } }); } + + private static final String SERVICE_ID = FirebaseAuth.class.getName(); + + private static class FirebaseAuthService extends FirebaseService { + + FirebaseAuthService(FirebaseApp app) { + super(SERVICE_ID, new FirebaseAuth(app)); + } + + @Override + public void destroy() { + // NOTE: We don't explicitly tear down anything here, but public methods of FirebaseAuth + // will now fail because calls to getCredential() will hit FirebaseApp.getOptions() which + // will throw once the app is deleted. + } + } } diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java index 68687da81..02b07f173 100644 --- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java +++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java @@ -1,6 +1,7 @@ package com.google.firebase.database; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; @@ -13,12 +14,15 @@ import com.google.firebase.database.utilities.ParsedUrl; import com.google.firebase.database.utilities.Utilities; import com.google.firebase.database.utilities.Validation; +import com.google.firebase.internal.FirebaseService; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; /** * The entry point for accessing a Firebase Database. You can get an instance by calling {@link @@ -30,22 +34,17 @@ public class FirebaseDatabase { private static final String ADMIN_SDK_PROPERTIES = "admin_sdk.properties"; private static final String SDK_VERSION = loadSdkVersion(); - /** - * A static map of FirebaseApp and RepoInfo to FirebaseDatabase instance. To ensure thread- - * safety, it should only be accessed in getInstance(), which is a synchronized method. - * - *

TODO(mikelehen): This serves a duplicate purpose as RepoManager. We should clean up. - * TODO(mikelehen): We should maybe be conscious of leaks and make this a weak map or similar but - * we have a lot of work to do to allow FirebaseDatabase/Repo etc. to be GC'd. - */ - private static final Map> - databaseInstances = new HashMap<>(); - private final FirebaseApp app; private final RepoInfo repoInfo; private final DatabaseConfig config; private Repo repo; // Usage must be guarded by a call to ensureRepo(). + private final AtomicBoolean destroyed = new AtomicBoolean(false); + + // Lock for synchronizing internal state changes. Protects accesses to repo and destroyed + // members. + private final Object lock = new Object(); + private FirebaseDatabase(FirebaseApp app, RepoInfo repoInfo, DatabaseConfig config) { this.app = app; this.repoInfo = repoInfo; @@ -97,19 +96,19 @@ public static FirebaseDatabase getInstance(FirebaseApp app) { * @return A FirebaseDatabase instance. */ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String url) { + FirebaseDatabaseService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID, + FirebaseDatabaseService.class); + if (service == null) { + service = ImplFirebaseTrampolines.addService(app, new FirebaseDatabaseService()); + } + + DatabaseInstances dbInstances = service.getInstance(); if (url == null || url.isEmpty()) { throw new DatabaseException( "Failed to get FirebaseDatabase instance: Specify DatabaseURL within " + "FirebaseApp or from your getInstance() call."); } - Map instances = databaseInstances.get(app.getName()); - - if (instances == null) { - instances = new HashMap<>(); - databaseInstances.put(app.getName(), instances); - } - ParsedUrl parsedUrl = Utilities.parseUrl(url); if (!parsedUrl.path.isEmpty()) { throw new DatabaseException( @@ -120,8 +119,7 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String + parsedUrl.path.toString()); } - FirebaseDatabase database = instances.get(parsedUrl.repoInfo); - + FirebaseDatabase database = dbInstances.get(parsedUrl.repoInfo); if (database == null) { DatabaseConfig config = new DatabaseConfig(); // If this is the default app, don't set the session persistence key so that we use our @@ -133,7 +131,7 @@ public static synchronized FirebaseDatabase getInstance(FirebaseApp app, String config.setFirebaseApp(app); database = new FirebaseDatabase(app, parsedUrl.repoInfo, config); - instances.put(parsedUrl.repoInfo, database); + dbInstances.put(parsedUrl.repoInfo, database); } return database; @@ -169,8 +167,7 @@ public FirebaseApp getApp() { * @return A DatabaseReference pointing to the root node. */ public DatabaseReference getReference() { - ensureRepo(); - return new DatabaseReference(this.repo, Path.getEmptyPath()); + return new DatabaseReference(ensureRepo(), Path.getEmptyPath()); } /** @@ -180,16 +177,11 @@ public DatabaseReference getReference() { * @return A DatabaseReference pointing to the specified path. */ public DatabaseReference getReference(String path) { - ensureRepo(); - - if (path == null) { - throw new NullPointerException( - "Can't pass null for argument 'pathString' in " + "FirebaseDatabase.getReference()"); - } + checkNotNull(path, + "Can't pass null for argument 'pathString' in FirebaseDatabase.getReference()"); Validation.validateRootPathString(path); - Path childPath = new Path(path); - return new DatabaseReference(this.repo, childPath); + return new DatabaseReference(ensureRepo(), childPath); } /** @@ -202,15 +194,11 @@ public DatabaseReference getReference(String path) { * @return A DatabaseReference for the provided URL. */ public DatabaseReference getReferenceFromUrl(String url) { - ensureRepo(); - - if (url == null) { - throw new NullPointerException( - "Can't pass null for argument 'url' in " + "FirebaseDatabase.getReferenceFromUrl()"); - } - + checkNotNull(url, + "Can't pass null for argument 'url' in FirebaseDatabase.getReferenceFromUrl()"); ParsedUrl parsedUrl = Utilities.parseUrl(url); - if (!parsedUrl.repoInfo.host.equals(this.repo.getRepoInfo().host)) { + Repo repo = ensureRepo(); + if (!parsedUrl.repoInfo.host.equals(repo.getRepoInfo().host)) { throw new DatabaseException( "Invalid URL (" + url @@ -218,8 +206,7 @@ public DatabaseReference getReferenceFromUrl(String url) { + "URL was expected to match configured Database URL: " + getReference().toString()); } - - return new DatabaseReference(this.repo, parsedUrl.path); + return new DatabaseReference(repo, parsedUrl.path); } /** @@ -233,8 +220,8 @@ public DatabaseReference getReferenceFromUrl(String url) { * listeners, and the client will not (re-)send them to the Firebase backend. */ public void purgeOutstandingWrites() { - ensureRepo(); - this.repo.scheduleNow( + final Repo repo = ensureRepo(); + repo.scheduleNow( new Runnable() { @Override public void run() { @@ -248,16 +235,14 @@ public void run() { * call. */ public void goOnline() { - ensureRepo(); - RepoManager.resume(this.repo); + RepoManager.resume(ensureRepo()); } /** * Shuts down our connection to the Firebase Database backend until {@link #goOnline()} is called. */ public void goOffline() { - ensureRepo(); - RepoManager.interrupt(this.repo); + RepoManager.interrupt(ensureRepo()); } /** @@ -269,8 +254,10 @@ public void goOffline() { * @param logLevel The desired minimum log level */ public synchronized void setLogLevel(Logger.Level logLevel) { - assertUnfrozen("setLogLevel"); - this.config.setLogLevel(logLevel); + synchronized (lock) { + assertUnfrozen("setLogLevel"); + this.config.setLogLevel(logLevel); + } } /** @@ -287,8 +274,10 @@ public synchronized void setLogLevel(Logger.Level logLevel) { * @param isEnabled Set to true to enable disk persistence, set to false to disable it. */ public synchronized void setPersistenceEnabled(boolean isEnabled) { - assertUnfrozen("setPersistenceEnabled"); - this.config.setPersistenceEnabled(isEnabled); + synchronized (lock) { + assertUnfrozen("setPersistenceEnabled"); + this.config.setPersistenceEnabled(isEnabled); + } } /** @@ -304,24 +293,44 @@ public synchronized void setPersistenceEnabled(boolean isEnabled) { * * @param cacheSizeInBytes The new size of the cache in bytes. */ - public synchronized void setPersistenceCacheSizeBytes(long cacheSizeInBytes) { - assertUnfrozen("setPersistenceCacheSizeBytes"); - this.config.setPersistenceCacheSizeBytes(cacheSizeInBytes); + public void setPersistenceCacheSizeBytes(long cacheSizeInBytes) { + synchronized (lock) { + assertUnfrozen("setPersistenceCacheSizeBytes"); + this.config.setPersistenceCacheSizeBytes(cacheSizeInBytes); + } } private void assertUnfrozen(String methodCalled) { - if (this.repo != null) { - throw new DatabaseException( - "Calls to " - + methodCalled - + "() must be made before any " - + "other usage of FirebaseDatabase instance."); + synchronized (lock) { + checkNotDestroyed(); + if (this.repo != null) { + throw new DatabaseException( + "Calls to " + + methodCalled + + "() must be made before any " + + "other usage of FirebaseDatabase instance."); + } } } - private synchronized void ensureRepo() { - if (this.repo == null) { - repo = RepoManager.createRepo(this.config, this.repoInfo, this); + /** + * Initializes the Repo if not already initialized. + */ + private Repo ensureRepo() { + synchronized (lock) { + checkNotDestroyed(); + if (repo == null) { + repo = RepoManager.createRepo(this.config, this.repoInfo, this); + } + return repo; + } + } + + void checkNotDestroyed() { + synchronized (lock) { + checkState(!destroyed.get(), + "FirebaseDatabase instance is no longer alive. This happens when " + + "the parent FirebaseApp instance has been deleted."); } } @@ -330,6 +339,21 @@ DatabaseConfig getConfig() { return this.config; } + void destroy() { + synchronized (lock) { + if (destroyed.get()) { + return; + } + + if (repo != null) { + RepoManager.interrupt(repo); + repo = null; + } + RepoManager.interrupt(getConfig()); + destroyed.compareAndSet(false, true); + } + } + private static String loadSdkVersion() { try (InputStream in = FirebaseDatabase.class.getClassLoader() .getResourceAsStream(ADMIN_SDK_PROPERTIES)) { @@ -340,4 +364,39 @@ private static String loadSdkVersion() { throw new RuntimeException(e); } } + + private static final String SERVICE_ID = FirebaseDatabase.class.getName(); + + private static class DatabaseInstances { + private final Map databases = + Collections.synchronizedMap(new HashMap()); + + void put(RepoInfo repo, FirebaseDatabase database) { + databases.put(repo, database); + } + + FirebaseDatabase get(RepoInfo repo) { + return databases.get(repo); + } + + void destroy() { + synchronized (databases) { + for (FirebaseDatabase database : databases.values()) { + database.destroy(); + } + databases.clear(); + } + } + } + + private static class FirebaseDatabaseService extends FirebaseService { + FirebaseDatabaseService() { + super(SERVICE_ID, new DatabaseInstances()); + } + + @Override + public void destroy() { + instance.destroy(); + } + } } diff --git a/src/main/java/com/google/firebase/database/InternalHelpers.java b/src/main/java/com/google/firebase/database/InternalHelpers.java index 6e5931bad..69ee09c70 100644 --- a/src/main/java/com/google/firebase/database/InternalHelpers.java +++ b/src/main/java/com/google/firebase/database/InternalHelpers.java @@ -35,4 +35,11 @@ public static FirebaseDatabase createDatabaseForTests( public static MutableData createMutableData(Node node) { return new MutableData(node); } + + /** + * For Repo to check if the database has been destroyed. + */ + public static void checkNotDestroyed(Repo repo) { + repo.getDatabase().checkNotDestroyed(); + } } diff --git a/src/main/java/com/google/firebase/database/Query.java b/src/main/java/com/google/firebase/database/Query.java index 07808ec2b..683a9439f 100644 --- a/src/main/java/com/google/firebase/database/Query.java +++ b/src/main/java/com/google/firebase/database/Query.java @@ -41,18 +41,15 @@ public class Query { private final boolean orderByCalled; Query(Repo repo, Path path, QueryParams params, boolean orderByCalled) throws DatabaseException { + hardAssert(params.isValid(), "Validation of queries failed."); this.repo = repo; this.path = path; this.params = params; this.orderByCalled = orderByCalled; - hardAssert(params.isValid(), "Validation of queries failed."); } Query(Repo repo, Path path) { - this.repo = repo; - this.path = path; - this.params = QueryParams.DEFAULT_PARAMS; - this.orderByCalled = false; + this(repo, path, QueryParams.DEFAULT_PARAMS, false); } /** diff --git a/src/main/java/com/google/firebase/database/core/Repo.java b/src/main/java/com/google/firebase/database/core/Repo.java index a1f247a4b..86a282d13 100644 --- a/src/main/java/com/google/firebase/database/core/Repo.java +++ b/src/main/java/com/google/firebase/database/core/Repo.java @@ -67,7 +67,7 @@ public class Repo implements PersistentConnection.Delegate { private long nextWriteId = 1; private SyncTree infoSyncTree; private SyncTree serverSyncTree; - private FirebaseDatabase database; + private final FirebaseDatabase database; private boolean loggedTransactionPersistenceWarning = false; private long transactionOrder = 0; @@ -268,11 +268,13 @@ public RepoInfo getRepoInfo() { } public void scheduleNow(Runnable r) { + InternalHelpers.checkNotDestroyed(this); ctx.requireStarted(); ctx.getRunLoop().scheduleNow(r); } public void postEvent(Runnable r) { + InternalHelpers.checkNotDestroyed(this); ctx.requireStarted(); ctx.getEventTarget().postEvent(r); } @@ -616,6 +618,7 @@ void interrupt() { } void resume() { + InternalHelpers.checkNotDestroyed(this); connection.resume(INTERRUPT_REASON); } diff --git a/src/main/java/com/google/firebase/internal/FirebaseService.java b/src/main/java/com/google/firebase/internal/FirebaseService.java new file mode 100644 index 000000000..3a33bcb09 --- /dev/null +++ b/src/main/java/com/google/firebase/internal/FirebaseService.java @@ -0,0 +1,50 @@ +package com.google.firebase.internal; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.base.Strings; + +/** + * Represents a service exposed from the Admin SDK (e.g. auth, database). Each instance of this + * class is associated with exactly one instance of FirebaseApp. Also provides a lifecycle hook + * to gracefully tear down the service. + * + * @param Type of the service + */ +public abstract class FirebaseService { + + private final String id; + protected final T instance; + + protected FirebaseService(String id, T instance) { + checkArgument(!Strings.isNullOrEmpty(id)); + this.id = id; + this.instance = checkNotNull(instance); + } + + /** + * Returns the ID used to identify this FirebaseService. Implementations must return a string + * unique to this service (e.g. full qualified class name of the service type). + * + * @return an ID string unique to this service type + */ + public final String getId() { + return id; + } + + /** + * Returns the concrete object instance that provides a specific Firebase service. + * + * @return the service object wrapped in this FirebaseService instance + */ + public final T getInstance() { + return instance; + } + + /** + * Tear down this FirebaseService instance and the service object wrapped in it, cleaning up + * any allocated resources in the process. + */ + public abstract void destroy(); +} diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java index 8ce6b8b2d..be7bd6776 100644 --- a/src/test/java/com/google/firebase/FirebaseAppTest.java +++ b/src/test/java/com/google/firebase/FirebaseAppTest.java @@ -3,6 +3,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; @@ -92,18 +94,42 @@ public void testRehydratingDeletedInstanceThrows() { } @Test - public void testDeleteCallback() { - String appName = "myApp"; - FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, appName); - FirebaseAppLifecycleListener listener = mock(FirebaseAppLifecycleListener.class); - firebaseApp.addLifecycleEventListener(listener); + public void testDeleteDefaultApp() { + FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS); + assertEquals(firebaseApp, FirebaseApp.getInstance()); firebaseApp.delete(); + try { + FirebaseApp.getInstance(); + fail(); + } catch (IllegalStateException expected) { + // ignore + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + } - verify(listener).onDeleted(appName, OPTIONS); - // Any further calls to delete are no-ops. - reset(listener); + @Test + public void testDeleteApp() { + final String name = "myApp"; + FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS, name); + assertSame(firebaseApp, FirebaseApp.getInstance(name)); firebaseApp.delete(); - verify(listener, never()).onDeleted(appName, OPTIONS); + + try { + FirebaseApp.getInstance(name); + fail(); + } catch (IllegalStateException expected) { + // ignore + } + + try { + // Verify we can reuse the same app name. + FirebaseApp firebaseApp2 = FirebaseApp.initializeApp(OPTIONS, name); + assertSame(firebaseApp2, FirebaseApp.getInstance(name)); + assertNotSame(firebaseApp, firebaseApp2); + } finally { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } } @Test diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java index a347a0023..74695d4f5 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java @@ -3,6 +3,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -17,6 +18,8 @@ import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.internal.FirebaseCustomAuthToken; import com.google.firebase.database.MapBuilder; +import com.google.firebase.internal.Log; +import com.google.firebase.tasks.Task; import com.google.firebase.tasks.Tasks; import com.google.firebase.testing.ServiceAccount; import com.google.firebase.testing.TestUtils; @@ -32,6 +35,9 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import org.json.JSONException; import org.json.JSONObject; import org.junit.After; @@ -49,6 +55,7 @@ public class FirebaseAuthTest { private static final String CLIENT_SECRET = "mockclientsecret"; private static final String CLIENT_ID = "mockclientid"; private static final String REFRESH_TOKEN = "mockrefreshtoken"; + private static final String TAG = "FirebaseAuthTest"; private final FirebaseOptions firebaseOptions; private final boolean isCertCredential; @@ -159,6 +166,58 @@ public void testGetInstanceForApp() throws ExecutionException, InterruptedExcept Assert.assertTrue(!token.isEmpty()); } + @Test + public void testAppDelete() throws ExecutionException, InterruptedException { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testAppDelete"); + FirebaseAuth auth = FirebaseAuth.getInstance(app); + assertNotNull(auth); + app.delete(); + try { + FirebaseAuth.getInstance(app); + fail("No error thrown when getting auth instance after deleting app"); + } catch (IllegalStateException expected) { + // ignore + } + } + + @Test + public void testInvokeAfterAppDelete() throws ExecutionException, InterruptedException { + if (!isCertCredential) { + Log.i(TAG, "Skipping testInvokeAfterAppDelete for non-cert credential"); + return; + } + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testInvokeAfterAppDelete"); + FirebaseAuth auth = FirebaseAuth.getInstance(app); + assertNotNull(auth); + app.delete(); + try { + auth.createCustomToken("foo"); + fail("No error thrown when invoking auth after deleting app"); + } catch (IllegalStateException expected) { + // ignore + } + } + + @Test + public void testInitAfterAppDelete() throws ExecutionException, InterruptedException, + TimeoutException { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); + FirebaseAuth auth1 = FirebaseAuth.getInstance(app); + assertNotNull(auth1); + app.delete(); + + app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); + FirebaseAuth auth2 = FirebaseAuth.getInstance(app); + assertNotNull(auth2); + assertNotSame(auth1, auth2); + + if (isCertCredential) { + Task task = auth2.createCustomToken("foo"); + assertNotNull(task); + assertNotNull(Tasks.await(task, TestUtils.TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); + } + } + @Test public void testAppWithAuthVariableOverrides() throws ExecutionException, InterruptedException { Map authVariableOverrides = Collections.singletonMap("uid", (Object) "uid1"); @@ -175,6 +234,7 @@ public void testAppWithAuthVariableOverrides() throws ExecutionException, Interr @Test public void testCreateCustomToken() throws Exception { if (!isCertCredential) { + Log.i(TAG, "Skipping testCreateCustomToken for non-cert credential"); return; } @@ -194,6 +254,7 @@ public void testCreateCustomToken() throws Exception { @Test public void testCreateCustomTokenWithDeveloperClaims() throws Exception { if (!isCertCredential) { + Log.i(TAG, "Skipping testCreateCustomTokenWithDeveloperClaims for non-cert credential"); return; } @@ -227,6 +288,7 @@ public void testServiceAccountUsedAsRefreshToken() throws Exception { @Test public void testCredentialCertificateRequired() throws Exception { if (isCertCredential) { + Log.i(TAG, "Skipping testCredentialCertificateRequired for cert credential"); return; } diff --git a/src/test/java/com/google/firebase/database/DataSnapshotTest.java b/src/test/java/com/google/firebase/database/DataSnapshotTest.java index ea20616ed..8e31510a9 100644 --- a/src/test/java/com/google/firebase/database/DataSnapshotTest.java +++ b/src/test/java/com/google/firebase/database/DataSnapshotTest.java @@ -8,6 +8,7 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseCredentials; +import com.google.firebase.database.core.DatabaseConfig; import com.google.firebase.database.snapshot.IndexedNode; import com.google.firebase.database.snapshot.Node; import com.google.firebase.database.snapshot.NodeUtilities; @@ -21,6 +22,7 @@ public class DataSnapshotTest { private static FirebaseApp testApp; + private static DatabaseConfig config; @BeforeClass public static void setUpClass() { @@ -29,17 +31,21 @@ public static void setUpClass() { .setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream())) .setDatabaseUrl("https://admin-java-sdk.firebaseio.com") .build()); + // Obtain a new DatabaseConfig instance for testing. Since we are not connecting to an + // actual Firebase database, it is necessary to use a stand-in DatabaseConfig here. + config = TestHelpers.newTestConfig(testApp); } @AfterClass - public static void tearDownClass() { + public static void tearDownClass() throws InterruptedException { + // Tear down and clean up the test DatabaseConfig. + TestHelpers.interruptConfig(config); TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } private DataSnapshot snapFor(Object data) { Node node = NodeUtilities.NodeFromJSON(data); - DatabaseReference ref = new DatabaseReference("https://test.firebaseio.com", TestHelpers - .newTestConfig(testApp)); + DatabaseReference ref = new DatabaseReference("https://test.firebaseio.com", config); return new DataSnapshot(ref, IndexedNode.from(node)); } diff --git a/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java new file mode 100644 index 000000000..bba52b4e6 --- /dev/null +++ b/src/test/java/com/google/firebase/database/FirebaseDatabaseTest.java @@ -0,0 +1,89 @@ +package com.google.firebase.database; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +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.FirebaseCredential; +import com.google.firebase.auth.FirebaseCredentials; +import com.google.firebase.testing.ServiceAccount; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.junit.AfterClass; +import org.junit.Test; + +public class FirebaseDatabaseTest { + + private static FirebaseOptions firebaseOptions = + new FirebaseOptions.Builder() + .setCredential(createCertificateCredential()) + .setDatabaseUrl("https://firebase-db-test.firebaseio.com") + .build(); + + @AfterClass + public static void tearDownClass() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + private static FirebaseCredential createCertificateCredential() { + return FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()); + } + + @Test + public void testGetInstance() throws ExecutionException, InterruptedException { + FirebaseApp.initializeApp(firebaseOptions); + FirebaseDatabase defaultDatabase = FirebaseDatabase.getInstance(); + assertNotNull(defaultDatabase); + assertSame(defaultDatabase, FirebaseDatabase.getInstance()); + } + + @Test + public void testGetInstanceForApp() throws ExecutionException, InterruptedException { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testGetInstanceForApp"); + FirebaseDatabase db = FirebaseDatabase.getInstance(app); + assertNotNull(db); + assertSame(db, FirebaseDatabase.getInstance(app)); + } + + @Test + public void testAppDelete() throws ExecutionException, InterruptedException { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testAppDelete"); + FirebaseDatabase db = FirebaseDatabase.getInstance(app); + assertNotNull(db); + app.delete(); + + try { + db.getReference(); + fail("No error thrown when calling method on database after delete"); + } catch (IllegalStateException expected) { + // ignore + } + + try { + FirebaseDatabase.getInstance(app); + fail("No error thrown when getting db instance after deleting app"); + } catch (IllegalStateException expected) { + // ignore + } + } + + @Test + public void testInitAfterAppDelete() throws ExecutionException, InterruptedException, + TimeoutException { + FirebaseApp app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); + FirebaseDatabase db1 = FirebaseDatabase.getInstance(app); + assertNotNull(db1); + app.delete(); + + app = FirebaseApp.initializeApp(firebaseOptions, "testInitAfterAppDelete"); + FirebaseDatabase db2 = FirebaseDatabase.getInstance(app); + assertNotNull(db2); + assertNotSame(db1, db2); + } +} diff --git a/src/test/java/com/google/firebase/database/TestHelpers.java b/src/test/java/com/google/firebase/database/TestHelpers.java index 8fc4cd08e..9534f97d7 100644 --- a/src/test/java/com/google/firebase/database/TestHelpers.java +++ b/src/test/java/com/google/firebase/database/TestHelpers.java @@ -9,6 +9,7 @@ import com.google.firebase.database.core.CoreTestHelpers; import com.google.firebase.database.core.DatabaseConfig; import com.google.firebase.database.core.Path; +import com.google.firebase.database.core.RepoManager; import com.google.firebase.database.core.view.QuerySpec; import com.google.firebase.database.future.WriteFuture; import com.google.firebase.database.snapshot.ChildKey; @@ -47,6 +48,19 @@ public static DatabaseConfig newTestConfig(FirebaseApp app) { return config; } + public static void interruptConfig(final DatabaseConfig config) throws InterruptedException { + RepoManager.interrupt(config); + long now = System.currentTimeMillis(); + synchronized (config) { + while (System.currentTimeMillis() - now < TestUtils.TEST_TIMEOUT_MILLIS) { + if (config.isStopped()) { + break; + } + config.wait(10); + } + } + } + public static DatabaseConfig getDatabaseConfig(FirebaseApp app) { return FirebaseDatabase.getInstance(app).getConfig(); } @@ -267,6 +281,10 @@ public static void assertAndUnwrapErrorHandlers(FirebaseApp app) { } } + public static void assertTimeDelta(long timestamp) { + assertTrue(Math.abs(System.currentTimeMillis() - timestamp) < TestUtils.TEST_TIMEOUT_MILLIS); + } + private static class TestExceptionHandler implements UncaughtExceptionHandler { private final AtomicReference throwable = new AtomicReference<>(); diff --git a/src/test/java/com/google/firebase/database/core/persistence/KeepSyncedTestIT.java b/src/test/java/com/google/firebase/database/core/persistence/KeepSyncedTestIT.java index 38971895a..20283b9e9 100644 --- a/src/test/java/com/google/firebase/database/core/persistence/KeepSyncedTestIT.java +++ b/src/test/java/com/google/firebase/database/core/persistence/KeepSyncedTestIT.java @@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableMap; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.EventRecord; import com.google.firebase.database.FirebaseDatabase; @@ -17,7 +16,6 @@ import java.util.List; import java.util.Map; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -29,12 +27,7 @@ public class KeepSyncedTestIT { @BeforeClass public static void setUpClass() throws IOException { - masterApp = IntegrationTestUtils.initDefaultApp(); - } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } @Before diff --git a/src/test/java/com/google/firebase/database/integration/DataTestIT.java b/src/test/java/com/google/firebase/database/integration/DataTestIT.java index e143668f0..d38f387dd 100644 --- a/src/test/java/com/google/firebase/database/integration/DataTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/DataTestIT.java @@ -11,7 +11,6 @@ import com.cedarsoftware.util.DeepEquals; import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -52,7 +51,6 @@ 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.Ignore; @@ -64,12 +62,7 @@ public class DataTestIT { @BeforeClass public static void setUpClass() { - masterApp = IntegrationTestUtils.initDefaultApp(); - } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } @Before @@ -2083,8 +2076,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { assertEquals(snap.getPriority().getClass(), Double.class); assertEquals(snap.getPriority(), snap.child("b").getPriority()); assertEquals(snap.child("a").getValue(), snap.child("b").getValue()); - assert (Math.abs( - System.currentTimeMillis() - Long.parseLong(snap.child("a").getValue().toString())) < 2000); + TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a").getValue().toString())); } @Test @@ -2145,8 +2137,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { TestHelpers.waitFor(opSemaphore); TestHelpers.waitFor(valSemaphore); - - assert (Math.abs(System.currentTimeMillis() - priority.get()) < 2000); + TestHelpers.assertTimeDelta(priority.get()); } @Test @@ -2198,8 +2189,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { DataSnapshot snap = readerEventRecord.getSnapshot(); assertEquals(snap.child("a/b/c").getValue().getClass(), Long.class); assertEquals(snap.child("a/b/d").getValue().getClass(), Long.class); - assert (Math.abs(System.currentTimeMillis() - - Long.parseLong(snap.child("a/b/c").getValue().toString())) < 2000); + TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a/b/c").getValue().toString())); } @Test @@ -2243,8 +2233,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { assertEquals(snap.getPriority().getClass(), Double.class); assertEquals(snap.getPriority(), snap.child("b").getPriority()); assertEquals(snap.child("a").getValue(), snap.child("b").getValue()); - assert (Math.abs( - System.currentTimeMillis() - Long.parseLong(snap.child("a").getValue().toString())) < 2000); + TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a").getValue().toString())); } @Test @@ -2303,8 +2292,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { TestHelpers.waitFor(opSemaphore); TestHelpers.waitFor(valSemaphore); - - assert (Math.abs(System.currentTimeMillis() - priority.get()) < 2000); + TestHelpers.assertTimeDelta(priority.get()); } @Test @@ -2352,8 +2340,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { DataSnapshot snap = readerEventRecord.getSnapshot(); assertEquals(snap.child("a/b/c").getValue().getClass(), Long.class); assertEquals(snap.child("a/b/d").getValue().getClass(), Long.class); - assert (Math.abs(System.currentTimeMillis() - - Long.parseLong(snap.child("a/b/c").getValue().toString())) < 2000); + TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a/b/c").getValue().toString())); } @Test @@ -2408,14 +2395,12 @@ public void onComplete(DatabaseError error, boolean committed, DataSnapshot curr EventRecord writerEventRecord = writerFuture.timedGet().get(1); DataSnapshot snap1 = writerEventRecord.getSnapshot(); assertEquals(snap1.getValue().getClass(), Long.class); - assert (Math - .abs(System.currentTimeMillis() - Long.parseLong(snap1.getValue().toString())) < 2000); + TestHelpers.assertTimeDelta(Long.parseLong(snap1.getValue().toString())); EventRecord readerEventRecord = readerFuture.timedGet().get(1); DataSnapshot snap2 = readerEventRecord.getSnapshot(); assertEquals(snap2.getValue().getClass(), Long.class); - assert (Math - .abs(System.currentTimeMillis() - Long.parseLong(snap2.getValue().toString())) < 2000); + TestHelpers.assertTimeDelta(Long.parseLong(snap2.getValue().toString())); } @Test diff --git a/src/test/java/com/google/firebase/database/integration/EventTestIT.java b/src/test/java/com/google/firebase/database/integration/EventTestIT.java index 8c04b9f66..aedb48075 100644 --- a/src/test/java/com/google/firebase/database/integration/EventTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/EventTestIT.java @@ -43,12 +43,7 @@ public class EventTestIT { @BeforeClass public static void setUpClass() throws TestFailure, TimeoutException, InterruptedException { - masterApp = IntegrationTestUtils.initDefaultApp(); - } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } @Before diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java index ccd3573ce..340d4e843 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseAuthTestIT.java @@ -6,7 +6,6 @@ import com.google.common.collect.ImmutableMap; 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; @@ -28,7 +27,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -39,15 +37,20 @@ public class FirebaseDatabaseAuthTestIT { @BeforeClass public static void setUpClass() throws IOException { - masterApp = IntegrationTestUtils.initDefaultApp(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); setDatabaseRules(); } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + + @Before + public void prepareApp() { + TestHelpers.wrapForErrorHandling(masterApp); } - + + @After + public void checkAndCleanupApp() { + TestHelpers.assertAndUnwrapErrorHandlers(masterApp); + } + @Test public void testAuthWithValidCertificateCredential() throws InterruptedException { FirebaseDatabase db = FirebaseDatabase.getInstance(); @@ -67,16 +70,6 @@ public void testAuthWithInvalidCertificateCredential() throws InterruptedExcepti // TODO: Ideally, we would find a way to verify the correct log output. assertWriteTimeout(db.getReference()); } - - @Before - public void prepareApp() { - TestHelpers.wrapForErrorHandling(masterApp); - } - - @After - public void checkAndCleanupApp() { - TestHelpers.assertAndUnwrapErrorHandlers(masterApp); - } @Test public void testDatabaseAuthVariablesAuthorization() throws InterruptedException { diff --git a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java index 8be5703e0..543f84031 100644 --- a/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/FirebaseDatabaseTestIT.java @@ -5,26 +5,48 @@ 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.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.FirebaseDatabase; +import com.google.firebase.database.MapBuilder; +import com.google.firebase.database.TestFailure; +import com.google.firebase.database.TestHelpers; +import com.google.firebase.database.ValueEventListener; +import com.google.firebase.database.future.ReadFuture; +import com.google.firebase.tasks.Tasks; import com.google.firebase.testing.IntegrationTestUtils; -import org.junit.AfterClass; +import com.google.firebase.testing.TestUtils; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public class FirebaseDatabaseTestIT { - + + private static FirebaseApp masterApp; + @BeforeClass public static void setUpClass() { - IntegrationTestUtils.initDefaultApp(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + + @Before + public void prepareApp() { + TestHelpers.wrapForErrorHandling(masterApp); + } + + @After + public void checkAndCleanupApp() { + TestHelpers.assertAndUnwrapErrorHandlers(masterApp); } @Test @@ -35,10 +57,9 @@ public void testGetDefaultInstance() { } @Test - public void testGetInstanceForApp() { - FirebaseApp app = appWithDbUrl(IntegrationTestUtils.getDatabaseUrl(), "testGetInstanceForApp"); - FirebaseDatabase db = FirebaseDatabase.getInstance(app); - assertEquals(app.getOptions().getDatabaseUrl(), db.getReference().toString()); + public void testGetInstanceForApp() throws InterruptedException, TestFailure, TimeoutException { + FirebaseDatabase db = FirebaseDatabase.getInstance(masterApp); + assertEquals(masterApp.getOptions().getDatabaseUrl(), db.getReference().toString()); } @Test @@ -95,7 +116,7 @@ public void testDatabaseUrlWithPathInGetInstance() { @Test public void testGetReference() { - FirebaseDatabase db = FirebaseDatabase.getInstance(); + FirebaseDatabase db = FirebaseDatabase.getInstance(masterApp); assertEquals(IntegrationTestUtils.getDatabaseUrl() + "/foo", db.getReference("foo").toString()); } @@ -103,7 +124,7 @@ public void testGetReference() { @Test public void testGetReferenceFromURLWithoutPath() { String dbUrl = IntegrationTestUtils.getDatabaseUrl(); - FirebaseDatabase db = FirebaseDatabase.getInstance(); + FirebaseDatabase db = FirebaseDatabase.getInstance(masterApp); DatabaseReference ref = db.getReferenceFromUrl(dbUrl); assertEquals(dbUrl, ref.toString()); } @@ -111,7 +132,7 @@ public void testGetReferenceFromURLWithoutPath() { @Test public void testGetReferenceFromURLWithPath() { String dbUrl = IntegrationTestUtils.getDatabaseUrl(); - FirebaseDatabase db = FirebaseDatabase.getInstance(); + FirebaseDatabase db = FirebaseDatabase.getInstance(masterApp); DatabaseReference ref = db.getReferenceFromUrl(dbUrl + "/foo/bar"); assertEquals(dbUrl + "/foo/bar", ref.toString()); } @@ -121,6 +142,101 @@ public void testGetReferenceThrowsWithBadUrl() { FirebaseDatabase db = FirebaseDatabase.getInstance(); db.getReferenceFromUrl("https://tests2.fake-firebaseio.com:9000"); } + + @Test + public void testSetValue() throws InterruptedException, ExecutionException, TimeoutException, + TestFailure { + FirebaseDatabase db = FirebaseDatabase.getInstance(masterApp); + DatabaseReference ref = db.getReference("testSetValue"); + Tasks.await(ref.setValue("foo"), TestUtils.TEST_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS); + ReadFuture readFuture = ReadFuture.untilEquals(ref, "foo"); + readFuture.timedWait(); + } + + @Test + public void testDeleteApp() throws InterruptedException, TestFailure, TimeoutException, + ExecutionException { + FirebaseApp app = IntegrationTestUtils.initApp("testDeleteApp"); + List ref = IntegrationTestUtils.getRandomNode(app, 2); + DatabaseReference writer = ref.get(0); + DatabaseReference reader = ref.get(1); + writer.setValue("test"); + TestHelpers.waitForRoundtrip(writer.getRoot()); + ReadFuture.untilEquals(reader, "test").timedWait(); + + app.delete(); + try { + IntegrationTestUtils.getRandomNode(app); + fail("No error thrown for deleted app"); + } catch (IllegalStateException expected) { + // ignore + } + + try { + writer.setValue("foo"); + fail("No error thrown for deleted app"); + } catch (IllegalStateException expected) { + // ignore + } + + try { + writer.updateChildren(MapBuilder.of("a", 1)); + fail("No error thrown for deleted app"); + } catch (IllegalStateException expected) { + // ignore + } + + try { + writer.addValueEventListener(new ValueEventListener() { + @Override + public void onDataChange(DataSnapshot snapshot) { + } + + @Override + public void onCancelled(DatabaseError error) { + } + }); + fail("No error thrown for deleted app"); + } catch (IllegalStateException expected) { + // ignore + } + + try { + writer.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) { + } + }); + fail("No error thrown for deleted app"); + } catch (IllegalStateException expected) { + // ignore + } + + app = IntegrationTestUtils.initApp("testDeleteApp"); + ref = IntegrationTestUtils.getRandomNode(app, 2); + writer = ref.get(0); + reader = ref.get(1); + writer.setValue("test2"); + TestHelpers.waitForRoundtrip(writer.getRoot()); + ReadFuture.untilEquals(reader, "test2").timedWait(); + } private static FirebaseApp appWithDbUrl(String dbUrl, String name) { FirebaseOptions options = new FirebaseOptions.Builder() diff --git a/src/test/java/com/google/firebase/database/integration/InfoTestIT.java b/src/test/java/com/google/firebase/database/integration/InfoTestIT.java index 47c5a7025..528deef4f 100644 --- a/src/test/java/com/google/firebase/database/integration/InfoTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/InfoTestIT.java @@ -6,7 +6,6 @@ import static org.junit.Assert.fail; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseException; @@ -23,7 +22,6 @@ import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -31,12 +29,7 @@ public class InfoTestIT { @BeforeClass public static void setUpClass() throws IOException { - IntegrationTestUtils.initDefaultApp(); - } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + IntegrationTestUtils.ensureDefaultApp(); } @Test diff --git a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java index cb4f0afd7..86588cc40 100644 --- a/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/OrderByTestIT.java @@ -5,7 +5,6 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -45,13 +44,12 @@ public class OrderByTestIT { @BeforeClass public static void setUpClass() { - masterApp = IntegrationTestUtils.initDefaultApp(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } @AfterClass public static void tearDownClass() throws IOException { uploadRules(masterApp, "{\"rules\": {\".read\": true, \".write\": true}}"); - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } @Before diff --git a/src/test/java/com/google/firebase/database/integration/OrderTestIT.java b/src/test/java/com/google/firebase/database/integration/OrderTestIT.java index f0f8daaea..2eeb3332e 100644 --- a/src/test/java/com/google/firebase/database/integration/OrderTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/OrderTestIT.java @@ -7,7 +7,6 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -29,7 +28,6 @@ import java.util.concurrent.TimeoutException; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -40,17 +38,12 @@ public class OrderTestIT { @BeforeClass public static void setUpClass() throws TestFailure, TimeoutException, InterruptedException { - masterApp = IntegrationTestUtils.initDefaultApp(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); // Make sure we're connected before any of these tests run DatabaseReference ref = FirebaseDatabase.getInstance(masterApp).getReference(); ReadFuture.untilEquals(ref.child(".info/connected"), true).timedGet(); } - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); - } - @Before public void prepareApp() { TestHelpers.wrapForErrorHandling(masterApp); diff --git a/src/test/java/com/google/firebase/database/integration/QueryTestIT.java b/src/test/java/com/google/firebase/database/integration/QueryTestIT.java index af74fdf12..fec348399 100644 --- a/src/test/java/com/google/firebase/database/integration/QueryTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/QueryTestIT.java @@ -9,7 +9,6 @@ import com.google.common.collect.ImmutableList; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -38,7 +37,6 @@ import java.util.concurrent.atomic.AtomicLong; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -49,12 +47,7 @@ public class QueryTestIT { @BeforeClass public static void setUpClass() throws IOException { - masterApp = IntegrationTestUtils.initDefaultApp(); - } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } @Before diff --git a/src/test/java/com/google/firebase/database/integration/RealtimeTestIT.java b/src/test/java/com/google/firebase/database/integration/RealtimeTestIT.java index c9d5eb331..d44998a78 100644 --- a/src/test/java/com/google/firebase/database/integration/RealtimeTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/RealtimeTestIT.java @@ -7,7 +7,6 @@ import static org.junit.Assert.fail; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.DatabaseReference; @@ -26,7 +25,6 @@ import com.google.firebase.database.utilities.Utilities; import com.google.firebase.testing.IntegrationTestUtils; -import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -35,7 +33,6 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; -import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; @@ -47,12 +44,7 @@ public class RealtimeTestIT { @BeforeClass public static void setUpClass() { - masterApp = IntegrationTestUtils.initDefaultApp(); - } - - @AfterClass - public static void tearDownClass() throws IOException { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } @Before @@ -737,8 +729,7 @@ public void onComplete(DatabaseError error, DatabaseReference ref) { assertEquals(snap.getPriority().getClass(), Double.class); assertEquals(snap.getPriority(), snap.child("b").getPriority()); assertEquals(snap.child("a").getValue(), snap.child("b").getValue()); - assert (Math.abs( - System.currentTimeMillis() - Long.parseLong(snap.child("a").getValue().toString())) < 2000); + TestHelpers.assertTimeDelta(Long.parseLong(snap.child("a").getValue().toString())); } // TODO: Find better way to test shutdown behavior. This test is not worth a diff --git a/src/test/java/com/google/firebase/database/integration/RulesTestIT.java b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java index ec179dfa9..3098875be 100644 --- a/src/test/java/com/google/firebase/database/integration/RulesTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/RulesTestIT.java @@ -48,8 +48,8 @@ public class RulesTestIT { - private static final String DEFAULT_RULES_STRING = - "{\n \"rules\": {\n \".read\": true,\n \".write\": true\n }\n}"; + private static final Map DEFAULT_RULES_STRING = + MapBuilder.of("rules", MapBuilder.of(".read", "auth != null", ".write", "auth != null")); private static final Map testRules; @@ -108,9 +108,8 @@ public static void setUpClass() throws IOException { @AfterClass public static void tearDownClass() throws IOException { - uploadRules(DEFAULT_RULES_STRING); + uploadRules(JsonMapper.serializeJson(DEFAULT_RULES_STRING)); TestHelpers.waitForRoundtrip(writer.getRoot()); - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); } @Before diff --git a/src/test/java/com/google/firebase/database/integration/TransactionTestIT.java b/src/test/java/com/google/firebase/database/integration/TransactionTestIT.java index f34290a9d..fb3027559 100644 --- a/src/test/java/com/google/firebase/database/integration/TransactionTestIT.java +++ b/src/test/java/com/google/firebase/database/integration/TransactionTestIT.java @@ -10,7 +10,6 @@ import com.google.common.collect.Iterables; import com.google.firebase.FirebaseApp; -import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; @@ -49,7 +48,6 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; -import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -62,12 +60,7 @@ public class TransactionTestIT { @BeforeClass public static void setUpClass() throws IOException { - masterApp = IntegrationTestUtils.initDefaultApp(); - } - - @AfterClass - public static void tearDownClass() { - TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + masterApp = IntegrationTestUtils.ensureDefaultApp(); } @Before diff --git a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java index ba84add63..3d2388a15 100644 --- a/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java +++ b/src/test/java/com/google/firebase/testing/IntegrationTestUtils.java @@ -34,6 +34,7 @@ public class IntegrationTestUtils { private static JSONObject IT_SERVICE_ACCOUNT; + private static FirebaseApp masterApp; private static synchronized JSONObject ensureServiceAccount() { if (IT_SERVICE_ACCOUNT == null) { @@ -62,13 +63,24 @@ public static String getDatabaseUrl() { return "https://" + getProjectId() + ".firebaseio.com"; } - public static FirebaseApp initDefaultApp() { - FirebaseOptions options = - new FirebaseOptions.Builder() - .setDatabaseUrl(getDatabaseUrl()) - .setCredential(FirebaseCredentials.fromCertificate(getServiceAccountCertificate())) - .build(); - return FirebaseApp.initializeApp(options); + /** + * Initializes the default FirebaseApp for integration testing (if not already initialized), and + * returns it. Integration tests that interact with the default FirebaseApp should call this + * method to obtain the app instance. This method ensures that all integration tests get the + * same FirebaseApp instance, instead of initializing an app per test. + * + * @return the default FirebaseApp instance + */ + public static synchronized FirebaseApp ensureDefaultApp() { + if (masterApp == null) { + FirebaseOptions options = + new FirebaseOptions.Builder() + .setDatabaseUrl(getDatabaseUrl()) + .setCredential(FirebaseCredentials.fromCertificate(getServiceAccountCertificate())) + .build(); + masterApp = FirebaseApp.initializeApp(options); + } + return masterApp; } public static FirebaseApp initApp(String name) {