If you're planning to unit test your Cloud Firestore security rules, you probably figured out you need to install the Firebase Emulator first. However, running your tests on a continuous integration environment might give you some headache.
So, let's go through some simple steps to test and deploy your Firestore security rules using the GitLab CI/CD runner.
For the sake of brevity, I'm assuming you're familiar with Firestore and have written your security rules. This guide will only teach how to automate your tests and deployment process.
Testing your Firestore rules
Before we start, you'll need to install the @firebase/testing
package to test your security rules:
yarn add @firebase/testing --dev
Now let's start testing our rules! First, let's create our firestore.rules.spec.ts
file (I'm using Typescript here but it's similar if you're using plain JS).
We need to import our @firebase/testing
module and fs
to read our security rules:
import * as firebase from '@firebase/testing';
import * as fs from 'fs';
Before we run our tests, let's load Firestore's security rules:
const projectId = 'my-firebase-project';
const rules = fs.readFileSync('firestore.rules', 'utf8');
beforeAll(async (done) => {
// Make your test app load your firestore rules
await firebase.loadFirestoreRules({ projectId, rules });
done();
});
Before each test case, we'll clear our Firestore data to avoid inconsistencies:
beforeEach(async (done) => {
// Reset our data from our test database
await firebase.clearFirestoreData({ projectId });
done();
});
We're going to test if only the author of a post can update it.
it('allows the author to update a post', async (done) => {
// Let's initialize the Admin SDK to populate some initial data
const admin = firebase.initializeAdminApp({ projectId }).firestore();
admin.doc('posts/123').set({ title: 'my post', authorId: 'leoDaVinci' });
// Then, we start our test app passing the `authorId` as the logged in user.
const auth = { uid: 'leoDaVinci' };
const app = firebase.initializeTestApp({ projectId, auth }).firestore();
const ref = app.doc('posts/123');
// Here we test if the `update` request succeeds.
await firebase.assertSucceeds(ref.update({ title: 'updated post' }));
done();
});
Now, we're going to test if anonymous users can update posts:
it('does not allow anonymous users to update a story', async (done) => {
const admin = firebase.initializeAdminApp({ projectId }).firestore();
admin.doc('stories/123').set({ name: 'my post', authorId: 'leoDaVinci' });
// Here, we'll initialize the test app passing an `undefined` user
const auth = undefined;
const app = firebase.initializeTestApp({ projectId, auth }).firestore();
const ref = app.doc('stories/123');
// Our `update` request should fail because the user isn't the same as the `authorId`
await firebase.assertFails(ref.update({ name: 'updated post' }));
done();
});
Running your tests locally
We're going to run our tests using Jest. Make sure you've installed it first:
yarn add jest @types-jest
Before we can run our tests, we need to install and start the Firebase Emulator:
# Install the emulator
firebase setup:emulators:firestore
# Start the emulator
firebase serve --only firestore
Keep it running in the background, and run your tests:
jest
Deploying your Firestore rules using GitLab
However, that setup won't work out of the box when running your tests on a continuous integration environment like the GitLab CI/CD runner.
The Firebase Emulator requires Java. We can use a Docker container which already installs both Java and the Firebase Emulator for us.
Create a .gitlab-ci.yml
file and use our custom Docker container:
image: wceolin/firebase-emulator
stages:
- test
test:
stage: test
script:
- yarn
- firebase serve --only firestore
- jest
However, we'll run into two problems:
- When running
firebase serve --only firestore
, the emulator won't be recognized - We can't run a
serve
job in parallel to running our tests
We can fix those issues by running the emulator's jar
directly and using the start-server-and-test
library to run both jobs together.
Let's start by adding a serve
and test
scripts to our package.json
:
"scripts": {
"serve": "java -jar $HOME/.cache/firebase/emulators/cloud-firestore-emulator-*.jar --host=127.0.0.1",
"test": "jest"
}
Now, let's run those scripts in your GitLab CI/CD environment:
image: wceolin/firebase-emulator
stages:
- test
test:
stage: test
script:
- yarn
- start-server-and-test serve http-get://127.0.0.1:8080 test
Now, it will start the emulator from the jar
file and run our test suite at the same time.
We can also automatically deploy it to Firebase by adding a new deploy
stage to our GitLab config file:
image: wceolin/firebase-emulator
stages:
- test
- deploy
test:
stage: test
script:
- yarn
- start-server-and-test serve http-get://127.0.0.1:8080 test
deploy:
stage: deploy
script:
- firebase deploy --only firestore --token "$FIREBASE_TOKEN"
That's it! Now your Firestore project can be automatically and deployed. :)
PS. You have a look at this repository for a production app using this setup.
Discussion (2)
Everything worked super fine until I upgraded my rules to use Map.diff as following: request.resource.data.diff(resource.data).affectedKeys().hasOnly('myKey').
Seems like the emulator can't find function called 'diff', though it works just fine locally.
Looking at your Dockerfile I couldn't find any issues as well. Any ideas what might be causing it?
Hi! Make sure you have the latest versions of
@firebase/testing
and the Firebase emulator installed. In my case, I've updated all Firebase dependencies (firebase, firebase-tools, @firebase/testing). After that, it automatically updated the Emulator once I started it.