DEV Community

loading...

How to test Firestore security rules using a CI environment

wceolin profile image Will Ceolin ・4 min read

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)

Collapse
kkaar profile image
KKaar

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?

Collapse
wceolin profile image
Will Ceolin Author • Edited

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.

Forem Open with the Forem app