diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java index 97821e6db00..0b118ab5a7b 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java @@ -41,6 +41,7 @@ public static FirebaseFirestore newFirebaseFirestore( asyncQueue, firebaseApp, instanceRegistry, + null, null); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 1a0c7b0a9eb..3daecfad7d3 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -27,6 +27,9 @@ import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.auth.internal.InternalAuthProvider; +import com.google.firebase.emulators.EmulatedServiceSettings; +import com.google.firebase.emulators.EmulatorSettings; +import com.google.firebase.emulators.FirebaseEmulator; import com.google.firebase.firestore.FirebaseFirestoreException.Code; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.EmptyCredentialsProvider; @@ -55,6 +58,15 @@ */ public class FirebaseFirestore { + /** + * Emulator identifier. See {@link FirebaseApp#enableEmulators(EmulatorSettings)} + * + *
TODO(samstern): Un-hide this once Firestore, Database, and Functions are implemented + * + * @hide + */ + public static FirebaseEmulator EMULATOR = FirebaseEmulator.forName("firestore"); + /** * Provides a registry management interface for {@code FirebaseFirestore} instances. * @@ -81,6 +93,7 @@ public interface InstanceRegistry { private FirebaseFirestoreSettings settings; private volatile FirestoreClient client; private final GrpcMetadataProvider metadataProvider; + private final EmulatedServiceSettings emulatorSettings; @NonNull public static FirebaseFirestore getInstance() { @@ -144,7 +157,8 @@ static FirebaseFirestore newInstance( queue, app, instanceRegistry, - metadataProvider); + metadataProvider, + app.getEmulatorSettings().getServiceSettings(EMULATOR)); return firestore; } @@ -157,7 +171,8 @@ static FirebaseFirestore newInstance( AsyncQueue asyncQueue, @Nullable FirebaseApp firebaseApp, InstanceRegistry instanceRegistry, - @Nullable GrpcMetadataProvider metadataProvider) { + @Nullable GrpcMetadataProvider metadataProvider, + @Nullable EmulatedServiceSettings emulatorSettings) { this.context = checkNotNull(context); this.databaseId = checkNotNull(checkNotNull(databaseId)); this.userDataReader = new UserDataReader(databaseId); @@ -168,8 +183,10 @@ static FirebaseFirestore newInstance( this.firebaseApp = firebaseApp; this.instanceRegistry = instanceRegistry; this.metadataProvider = metadataProvider; + this.emulatorSettings = emulatorSettings; - settings = new FirebaseFirestoreSettings.Builder().build(); + this.settings = new FirebaseFirestoreSettings.Builder().build(); + this.settings = mergeEmulatorSettings(settings, emulatorSettings); } /** Returns the settings used by this {@code FirebaseFirestore} object. */ @@ -185,6 +202,8 @@ public FirebaseFirestoreSettings getFirestoreSettings() { public void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { synchronized (databaseId) { checkNotNull(settings, "Provided settings must not be null."); + settings = mergeEmulatorSettings(settings, emulatorSettings); + // As a special exception, don't throw if the same settings are passed repeatedly. This // should make it simpler to get a Firestore instance in an activity. if (client != null && !this.settings.equals(settings)) { @@ -193,6 +212,7 @@ public void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { + "You can only call setFirestoreSettings() before calling any other methods on a " + "FirebaseFirestore object."); } + this.settings = settings; } } @@ -215,6 +235,24 @@ private void ensureClientConfigured() { } } + private FirebaseFirestoreSettings mergeEmulatorSettings( + @NonNull FirebaseFirestoreSettings settings, + @Nullable EmulatedServiceSettings emulatorSettings) { + if (emulatorSettings == null) { + return settings; + } + + if (!FirebaseFirestoreSettings.DEFAULT_HOST.equals(settings.getHost())) { + throw new IllegalStateException( + "Cannot specify the host in FirebaseFirestoreSettings when EmulatedServiceSettings is provided."); + } + + return new FirebaseFirestoreSettings.Builder(settings) + .setHost(emulatorSettings.getHost() + ":" + emulatorSettings.getPort()) + .setSslEnabled(false) + .build(); + } + /** Returns the FirebaseApp instance to which this {@code FirebaseFirestore} belongs. */ @NonNull public FirebaseApp getApp() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestoreSettings.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestoreSettings.java index cf18e2000ad..2c488e61f75 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestoreSettings.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestoreSettings.java @@ -27,9 +27,11 @@ public final class FirebaseFirestoreSettings { */ public static final long CACHE_SIZE_UNLIMITED = -1; + /** @hide */ + public static final String DEFAULT_HOST = "firestore.googleapis.com"; + private static final long MINIMUM_CACHE_BYTES = 1 * 1024 * 1024; // 1 MB private static final long DEFAULT_CACHE_SIZE_BYTES = 100 * 1024 * 1024; // 100 MB - private static final String DEFAULT_HOST = "firestore.googleapis.com"; private static final boolean DEFAULT_TIMESTAMPS_IN_SNAPSHOTS_ENABLED = true; /** A Builder for creating {@code FirebaseFirestoreSettings}. */ diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTest.java new file mode 100644 index 00000000000..13a28b547af --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTest.java @@ -0,0 +1,176 @@ +// Copyright 2020 Google LLC +// +// 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.firestore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import androidx.annotation.NonNull; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.emulators.EmulatedServiceSettings; +import com.google.firebase.emulators.EmulatorSettings; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class FirebaseFirestoreTest { + + @Test + public void getInstance_withEmulator() { + FirebaseApp app = getApp("getInstance_withEmulator"); + + app.enableEmulators( + new EmulatorSettings.Builder() + .addEmulatedService( + FirebaseFirestore.EMULATOR, new EmulatedServiceSettings("10.0.2.2", 8080)) + .build()); + + FirebaseFirestore firestore = FirebaseFirestore.getInstance(app); + FirebaseFirestoreSettings settings = firestore.getFirestoreSettings(); + + assertEquals(settings.getHost(), "10.0.2.2:8080"); + assertFalse(settings.isSslEnabled()); + } + + @Test + public void getInstance_withEmulator_mergeSettingsSuccess() { + FirebaseApp app = getApp("getInstance_withEmulator_mergeSettingsSuccess"); + app.enableEmulators( + new EmulatorSettings.Builder() + .addEmulatedService( + FirebaseFirestore.EMULATOR, new EmulatedServiceSettings("10.0.2.2", 8080)) + .build()); + + FirebaseFirestore firestore = FirebaseFirestore.getInstance(app); + firestore.setFirestoreSettings( + new FirebaseFirestoreSettings.Builder().setPersistenceEnabled(false).build()); + + FirebaseFirestoreSettings settings = firestore.getFirestoreSettings(); + + assertEquals(settings.getHost(), "10.0.2.2:8080"); + assertFalse(settings.isSslEnabled()); + assertFalse(settings.isPersistenceEnabled()); + } + + @Test + public void getInstance_withEmulator_mergeSettingsFailure() { + FirebaseApp app = getApp("getInstance_withEmulator_mergeSettingsFailure"); + app.enableEmulators( + new EmulatorSettings.Builder() + .addEmulatedService( + FirebaseFirestore.EMULATOR, new EmulatedServiceSettings("10.0.2.2", 8080)) + .build()); + + try { + FirebaseFirestore firestore = FirebaseFirestore.getInstance(app); + firestore.setFirestoreSettings( + new FirebaseFirestoreSettings.Builder().setHost("myhost.com").build()); + fail("Exception should be thrown"); + } catch (Exception e) { + assertTrue(e instanceof IllegalStateException); + assertEquals( + e.getMessage(), + "Cannot specify the host in FirebaseFirestoreSettings when EmulatedServiceSettings is provided."); + } + } + + @Test + public void setSettings_repeatedSuccess() { + FirebaseApp app = getApp("setSettings_repeatedSuccess"); + FirebaseFirestore firestore = FirebaseFirestore.getInstance(app); + + FirebaseFirestoreSettings settings = + new FirebaseFirestoreSettings.Builder().setHost("myhost.com").setSslEnabled(false).build(); + firestore.setFirestoreSettings(settings); + + // This should 'start' Firestore + DocumentReference reference = firestore.document("foo/bar"); + + // Second settings set should pass because the settings are equal + firestore.setFirestoreSettings(settings); + } + + @Test + public void setSettings_repeatedSuccess_withEmulator() { + FirebaseApp app = getApp("setSettings_repeatedSuccess_withEmulator"); + app.enableEmulators( + new EmulatorSettings.Builder() + .addEmulatedService( + FirebaseFirestore.EMULATOR, new EmulatedServiceSettings("10.0.2.2", 8080)) + .build()); + + FirebaseFirestore firestore = FirebaseFirestore.getInstance(app); + + FirebaseFirestoreSettings settings = + new FirebaseFirestoreSettings.Builder().setPersistenceEnabled(false).build(); + firestore.setFirestoreSettings(settings); + + // This should 'start' Firestore + DocumentReference reference = firestore.document("foo/bar"); + + // Second settings set should pass because the settings are equal + firestore.setFirestoreSettings(settings); + } + + @Test + public void setSettings_repeatedFailure() { + FirebaseApp app = getApp("setSettings_repeatedFailure"); + FirebaseFirestore firestore = FirebaseFirestore.getInstance(app); + + FirebaseFirestoreSettings settings = + new FirebaseFirestoreSettings.Builder().setHost("myhost.com").setSslEnabled(false).build(); + + FirebaseFirestoreSettings otherSettings = + new FirebaseFirestoreSettings.Builder() + .setHost("otherhost.com") + .setSslEnabled(false) + .build(); + + firestore.setFirestoreSettings(settings); + + // This should 'start' Firestore + DocumentReference reference = firestore.document("foo/bar"); + + try { + firestore.setFirestoreSettings(otherSettings); + fail("Exception should be thrown"); + } catch (Exception e) { + assertTrue(e instanceof IllegalStateException); + assertTrue( + e.getMessage() + .startsWith( + "FirebaseFirestore has already been started and its settings can no longer be changed.")); + } + } + + @NonNull + private FirebaseApp getApp(@NonNull String name) { + return FirebaseApp.initializeApp( + InstrumentationRegistry.getInstrumentation().getContext(), + new FirebaseOptions.Builder() + .setApplicationId("appid") + .setApiKey("apikey") + .setProjectId("projectid") + .build(), + name); + } +}