As you're building your app, you might want to lock down access to your Firestore database. However, before you launch, you'll need more nuanced Firestore Security Rules. With the Firestore emulator, you can write unit tests that check the behavior of your Firestore Security Rules.
Quickstart
For a few basic test cases with simple rules, try out the quickstart sample.
Understand Firestore Security Rules
Implement Firebase Authentication and Firestore Security Rules for serverless authentication, authorization, and data validation when you use the mobile and web client libraries.
Firestore Security Rules include two pieces:
- A
match
statement that identifies documents in your database. - An
allow
expression that controls access to those documents.
Firebase Authentication verifies users' credentials and provides the foundation for user-based and role-based access systems.
Every database request from a Firestore mobile/web client library is evaluated against your security rules before reading or writing any data. If the rules deny access to any of the specified document paths, the entire request fails.
Learn more about Firestore Security Rules in Get started with Firestore Security Rules.
Install the emulator
To install the Firestore emulator, use the Firebase CLI and run the command below:
firebase setup:emulators:firestore
Run the emulator
Start the emulator using the following command. The emulator will run until you kill the process:
firebase emulators:start --only firestore
In many cases you want to start the emulator, run a test suite, and then shut
down the emulator after the tests run. You can do this easily using the
emulators:exec
command:
firebase emulators:exec --only firestore "./my-test-script.sh"
When started the emulator will attempt to run on a default port (8080). You can
change the emulator port by modifying the "emulators"
section of your
firebase.json
file:
{ // ... "emulators": { "firestore": { "port": "YOUR_PORT" } } }
Before you run the emulator
Before you start using the emulator, keep in mind the following:
- The emulator will initially load the rules specified in the
firestore.rules
field of yourfirebase.json
file. It expects the name of a local file containing your Firestore Security Rules and applies those rules to all projects. If you don't provide the local file path or use theloadFirestoreRules
method as described below, the emulator treats all projects as having open rules. - While
most Firebase SDKs
work with the emulators directly, only the
@firebase/rules-unit-testing
library supports mockingauth
in Security Rules, making unit tests much easier. In addition, the library supports a few emulator-specific features like clearing all data, as listed below. - The emulators will also accept production Firebase Auth tokens provided through Client SDKs and evaluate rules accordingly, which allows connecting your application directly to the emulators in integration and manual tests.
Run local tests
initializeTestApp({ projectId: string, auth: Object }) => FirebaseApp
This method returns an initialized Firebase app corresponding to the project ID and auth variable specified in the options. Use this to create an app authenticated as a specific user to use in tests.
firebase.initializeTestApp({ projectId: "my-test-project", auth: { uid: "alice", email: "alice@example.com" } });
initializeAdminApp({ projectId: string }) => FirebaseApp
This method returns an initialized admin Firebase app. This app bypasses security rules when performing reads and writes. Use this to create an app authenticated as an admin to set state for tests.
firebase.initializeAdminApp({ projectId: "my-test-project" });
apps() => [FirebaseApp]
This method returns all the currently initialized test and admin apps.
Use this to clean up apps between or after tests.
Promise.all(firebase.apps().map(app => app.delete()))
loadFirestoreRules({ projectId: string, rules: Object }) => Promise
This method sends rules to a locally running database. It takes an object that specifies the rules as a string. Use this method to set your database's rules.
firebase.loadFirestoreRules({ projectId: "my-test-project", rules: fs.readFileSync("/path/to/firestore.rules", "utf8") });
assertFails(pr: Promise) => Promise
This method returns a promise that is rejected if the input succeeds or that succeeds if the input is rejected. Use this to assert if a database read or write fails.
firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get());
assertSucceeds(pr: Promise) => Promise
This method returns a promise that succeeds if the input succeeds and is rejected if the input is rejected. Use this to assert if a database read or write succeeds.
firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get());
clearFirestoreData({ projectId: string }) => Promise
This method clears all data associated with a particular project in the locally running Firestore instance. Use this method to clean-up after tests.
firebase.clearFirestoreData({ projectId: "my-test-project" });
Generate test reports
After running a suite of tests, you can access test coverage reports that show how each of your security rules was evaluated.
To get the reports, query an exposed endpoint on the emulator while it's running. For a browser-friendly version, use the following URL:
http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html
This breaks your rules into expressions and subexpressions that you can mouseover for more information, including number of evaluations and values returned. For the raw JSON version of this data, include the following URL in your query:
http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage
Differences between the emulator and production
- You do not have to explicitly create a Firestore project. The emulator automatically creates any instance that is accessed.
- The Firestore emulator does not work with the normal Firebase Authentication flow.
Instead, in the Firebase Test SDK, we have provided the
initializeTestApp()
method in therules-unit-testing
library, which takes anauth
field. The Firebase handle created using this method will behave as though it has successfully authenticated as whatever entity you provide. If you pass innull
, it will behave as an unauthenticated user (auth != null
rules will fail, for example).
Troubleshoot known issues
As you use the Firestore emulator, you might run into the following known issues. Follow the guidance below to troubleshoot any irregular behavior you're experiencing. These notes are written with the Firebase Test SDK in mind, but the general approaches are applicable to any Firebase SDK.
Test behavior is inconsistent
If your tests are occasionally passing and failing, even without any changes to
the tests themselves, you might need to verify that they're properly sequenced.
Most interactions with the emulator are asynchronous, so double-check that all
the async code is properly sequenced. You can fix the sequencing by either
chaining promises, or using await
notation liberally.
In particular, review the following async operations:
- Setting security rules, with, for example,
firebase.loadFirestoreRules
. - Reading and writing data, with, for example,
db.collection("users").doc("alice").get()
. - Operational assertions, including
firebase.assertSucceeds
andfirebase.assertFails
.
Tests only pass the first time you load the emulator
The emulator is stateful. It stores all the data written to it in memory, so any data is lost whenever the emulator shuts down. If you're running multiple tests against the same project id, each test can produce data that might influence subsequent tests. You can use any of the following methods to bypass this behavior:
- Use unique project IDs for each test. Note that if you choose to do this you
will need to call
loadFirestoreRules
as part of each test; rules are only automatically loaded for the default project ID. - Restructure your tests so they don't interact with previously written data (for example, use a different collection for each test).
- Delete all the data written during a test.
Test setup is very complicated
You might want to test scenarios that your Firestore Security Rules don't actually allow. For example, testing whether unauthenticated users can edit data is difficult to test, since you can't edit data as an unauthenticated user.
If your rules are making test setup complex, try using an admin-authorized
client to bypass the rules. You can do this with firebase.initializeAdminApp
.
Reads and writes from admin-authorized clients bypass rules and don't trigger
PERMISSION_DENIED
errors.