Test your Cloud Firestore Security Rules

As you're building your app, you might want to lock down access to your Cloud Firestore database. However, before you launch, you'll need more nuanced Cloud Firestore Security Rules. With the Cloud Firestore emulator, you can write unit tests that check the behavior of your Cloud Firestore Security Rules.

Quickstart

For a few basic test cases with simple rules, try out the JavaScript quickstart or the TypeScript quickstart.

Understand Cloud Firestore Security Rules

Implement Firebase Authentication and Cloud Firestore Security Rules for serverless authentication, authorization, and data validation when you use the mobile and web client libraries.

Cloud Firestore Security Rules include two pieces:

  1. A match statement that identifies documents in your database.
  2. 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 Cloud 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 Cloud Firestore Security Rules in Get started with Cloud Firestore Security Rules.

Install the emulator

To install the Cloud Firestore emulator, use the Firebase CLI and follow the steps below.

  1. To opt-in to the emulator beta, run the following command:

     firebase --open-sesame emulators
     firebase setup:emulators:firestore
     

  2. Start the emulator using the following command. The emulator runs during all your tests.

     firebase serve --only firestore
     

Before you run the emulator

Before you start using the emulator, keep in mind the following:

  • The only SDK that currently supports the emulator is the Node.js SDK. We've provided the @firebase/testing module to make it easier to interact with the emulator.
  • The emulator supports an optional --rules CLI flag. It expects the name of a local file containing your Cloud Firestore Security Rules and applies those rules to all projects. If you don't provide the local file path or use the loadFirestoreRules method as described below, the emulator treats all projects as having open rules.

Run local tests

Use the @firebase/testing module to interact the emulator that runs locally. If you get timeouts or ECONNREFUSED errors, double-check that the emulator is actually running.

We strongly recommend using a recent version of Node.js so you can use async/await notation. Almost all of the behavior you might want to test involves asynchronous functions, and the testing module is designed to work with Promise-based code.

The module exposes the following methods:

  • initializeTestApp({ projectId: <name>, auth: <auth> }) => 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: <name> }) => 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: <name>, rules: <rules> }) => 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());
    

Differences between the emulator and production

  1. You do not have to explicitly create a Cloud Firestore project. The emulator automatically creates any instance that is accessed.
  2. The Cloud Firestore emulator does not have working interaction with other Firebase products. Notably, the normal Firebase Authentication flow will not work. Instead, we have provided the initializeTestApp() method in the testing module, which takes an auth field. The Firebase handle created using this method will behave as though it has successfully authenticated as whatever entity you provide. If you pass in null, it will behave as an unauthenticated user (auth != null rules will fail, for example).
  3. The Cloud Firestore emulator persists data. This might impact your results. To run tests independently, assign a different project ID for each, independent test. When you call firebase.initializeAdminApp or firebase.initializeTestApp, append a unique ID, timestamp, or random integer to the projectID. You can also resolve race conditions by waiting for promises to resolve through async or await keywords in JavaScript.

Troubleshoot known issues

As you use the Cloud Firestore emulator, you might run into the following known issues. Follow the guidance below to troubleshoot any irregular behavior you're experiencing.

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 and firebase.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.
  • 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 Cloud 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.

Send feedback about...

Need help? Visit our support page.