Testing Firestore rules using Emulator Suite

Testing Firestore rules using Emulator Suite

Testers succeed, where others fail…

This year, Firebase introduced changes into their Emulator Suite, which allows testing at ease….

Begin…

Firebase Local Emulator Suite consists of individual Firebase service emulators built to mimic the behavior of Firebase services. This means you can connect your app directly to these emulators to perform integration testing or QA without touching production data.

For example, you could connect your app to the Firestore emulator to safely read and write documents in testing. These writes may trigger functions in the Cloud Functions emulator. However your app will still continue to communicate with production Firebase services when emulators are not available or configured.

Firebase Emulator Suite….
Firebase Emulator Suite….

What’s supported…?

As per the official Google Documentation, 

Firebase Emulator Suite allows you to test your code with our core BaaS products in an interoperable way. The Cloud Functions emulator supports HTTP functions, callable functions and background functions triggered by Firestore and Realtime Database; the Cloud Functions emulator does not support background functions triggered by Auth or Cloud Storage for Firebase. For Firestore and Realtime Database emulators, Security Rules emulation is built in.

Copying Firestore rules….

  1. Using Firebase CLI, run the command below:
firebase init firestore

After configuring correctly, it will create a .rules file (firestore.rules)

2. For deploying rules changes to the Firestore, you can edit the firestore.rules file and once confirmed, run the following command :

firebase deploy --only firestore:rule

You should see your changes under the rules section….

Installing Firebase Emulator Suite…

  1. Install the Emulator…..

Using Firebase CLI, run the command below:

firebase setup:emulators:firestore

2. Setup your project for test……

firebase use --add your-project-name

3. Run the Emulator, 

firebase emulators:start --only firestore

The emulator will run until you kill the process….

If everything configured well, you should see something like this……

Testing Firestore rules using Emulator Suite
Testing Firestore rules using Emulator Suite

Note: If any errors regarding the port number, you can make changes in firebase.json file, by including the “emulators” section…

{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"emulators": {
"firestore": {
"port": 8080
}
}
}

Testing Firestore using Emulator Suite….

Until now, we did the following things:

  1. Copying the firestore rules from our project.
  2. Installing and running the emulator suite…

Now, comes the time to test our existing firestore rules….

  1. Create a test folder and initialize a test.js file….
Testing Firestore rules using Emulator Suite
Testing Firestore rules using Emulator Suite

This is project structure so far…..

2. Use the @firebase/testing module to interact the emulator that runs locally.

Inside your package.json, include the dependencies as : 

"devDependencies": {
"@firebase/testing": "^0.14.1",
"filesystem": "^1.0.1",
"mocha": "^6.2.2",
"prettier": "^1.18.2"
}

and then run : 

npm install

3. The module firebase/testing includes the following methods:

– initializeTestApp

firebase.initializeTestApp({
projectId: "my-test-project",
auth: { uid: "alice", email: "alice@example.com" }
});
  • initializeAdminApp
firebase.initializeAdminApp({ projectId: "my-test-project" });
  • apps
Promise.all(firebase.apps().map(app => app.delete()))
  • loadFirestoreRules
firebase.loadFirestoreRules({
projectId: "my-test-project",
rules: fs.readFileSync("/path/to/firestore.rules", "utf8")
});
  • assertFails
firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get());
  • assertSucceeds
firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get());
  • clearFirestoreData
firebase.clearFirestoreData({
projectId: "my-test-project"
});

Note : For detailed info, about each method, visit this link.


Setup for Test….

Inside test.js file, add these lines…

const firebase = require("@firebase/testing");
const fs = require("fs");
const projectId = "YOUR PROJECT NAME";
const firebasePort = require("../firebase.json").emulators.firestore.port;
const port = firebasePort ? firebasePort : 8080;
const rules = fs.readFileSync("firestore.rules", "utf8");
function authedApp(auth) {
return firebase.initializeTestApp({ projectId, auth }).firestore();
}

In above snippet, you specify :

  • Firebase Project Id
  • Firebase Port , Firestore rules

Next, add the following lines :

beforeEach(async () => {
await firebase.clearFirestoreData({ projectId });
});
before(async () => {
await firebase.loadFirestoreRules({ projectId, rules });
});
after(async () => {
await Promise.all(firebase.apps().map(app => app.delete()));
});

In above snippet, you implement :

  • beforeEach : 

This method clears all data associated with a particular project in the locally running Firestore instance. Use this method to clean-up after tests.

  • before :

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.

  • after :

This method returns all the currently initialized test and admin apps. Use this to clean up apps between or after tests.

Note : Data is not deleted from Firestore, only from the local running db….

Writing Test….

All the test cases, should be written under :

describe("YOUR TEST SUITE NAME", () => {
it("Test case 1", async () => {
});
it("Test case 2", async () => {
});
});

Following are the test cases :

  1. User should be logged in, before doing some Firestore related action
it("require user to log in before doing firestore action", async () => {
const db = authedApp(null);
const profile = db.collection("templates").doc("id2");
await firebase.assertFails(profile.set({ birthday: "January 1" }));
});

authedApp is the function, which we configured above….

Here, if the user id is not authenticated/null, firestore should not allow any action. (assertFails)…

2. Any user can access a particular collection as long as it’s authenticated

it("should let anyone read wonder (wonder is the collection name)", async () => {
const db = authedApp({ uid: "test" });
const profile = db.collection("wonder").doc("alice");
await firebase.assertSucceeds(profile.get());
});

Here, user authenticated is test, but we are trying to access alice (another user) details….

3. Only user should be able to create their profiles,

it("should only let user create their own profile", async () => {
const db = authedApp({ uid: "alice" });
await firebase.assertSucceeds(
db
.collection("profile")
.doc("alice")
.set({
birthday: "January 1",
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
);
await firebase.assertFails(
db
.collection("profile")
.doc("bob")
.set({
birthday: "January 1",
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
);
});

Here, alice is authenticated, in part one, she makes some actions in Firestore..This succeeds….

Now, bob tries to make some actions in Firestore..This fails…


How to run the tests….

Just run, 

npm test

You should see something like this,

Testing Firestore rules using Emulator Suite
Testing Firestore rules using Emulator Suite

Source Code :

https://github.com/AseemWangoo/flutter_programs/blob/master/firestore-emulator.zip