Firebase-tools: client.serve API is difficult to integrate into tests

Created on 15 Dec 2018  路  7Comments  路  Source: firebase/firebase-tools

Version info

6.1.1

Platform Information

OS X

Steps to reproduce

Using the API to start a firestore emulator I make the following call in the beforeAll of my firestore rules tests

    import client from 'firebase-tools';

    ...

    client.serve({
      only: 'firestore',
    }).then(() => {
      // do stuff when server has started
    });

Expected behavior

I should be able to do stuff once the server has started and later have a method for stopping the server

Actual behavior

The promise from client.serve actually only resolves once the server has stopped and the only way to stop the server is to send the process a SIGINT. This makes it difficult to use and possibly useless as the only way I can think of to sensibly do this is in a child process and in which case I may as well use the CLI

firestore feature request question

All 7 comments

ooo - I like this use-case! I'm not sure off-hand what would need to change to make that work, but maybe that's something to dig into (I'm not saying I have the time right now). If you wanted to explore this, I'd be happy to review any PRs as well.

In the meantime, if there's another way to do this via script, @ryanpbrewster might now as he's familiar with the emulators...

This seems totally reasonable. The emulators are just child processes that are running, and the Node library we're using to drive them already exposes the right primitives. I'll look into an implementation.

FWIW, I implemented something similar for one of my projects not too long ago. You can check it out here.

Edit: I just run those tests against the emulator (I usually use the Firestore backend) and something seems to have broken in the last couple of months. I'll try to fix that tomorrow but the general idea still applies.

If you're using Jest you can simply do this:

in globalSetup file:

const { start } = require('firebase-tools/lib/serve/firestore')

module.exports = start

in globalTeardown file:

const { stop } = require('firebase-tools/lib/serve/firestore')

module.exports = stop

Works great for me.

This would be a really great feature to run tests for Firestore rules. There are a few points I would like to point out which don't quite scale.

  • loadFirestoreRules(...) load the rules globally. So after loading them it's impossible to add test data.
  • clearFirestoreData(...) doesn't clear the rules. Initializing an app with the same project id will still have the rules loaded.
  • start() and stop() don't actually wait until the service is fully started or stopped.

I got around those issues by creating a new project id for each test and adding test data before loading the rules. Utils.wait() is just a setTimeout() which resolves after a certain amount of milliseconds, to give the service some time to boot up and shut down.

import * as firebase from "@firebase/testing";
import * as fs from "fs";
import {start, stop} from 'firebase-tools/lib/serve/firestore';
import {Utils} from '../../../app/utils/utils';

class FirestoreRuleTest {

    readonly projectId = 'ls-test-' + Date.now();

    readonly firestore: firebase.firestore.Firestore;

    constructor(uid?: string) {
        const auth = uid ? {uid} : null;
        this.firestore = firebase
            .initializeTestApp({
                projectId: this.projectId,
                auth
            })
            .firestore();
    }

    async loadRules() {
        const rules = fs.readFileSync(`${__dirname}/../../../../../firestore.rules`, 'utf8');
        await firebase.loadFirestoreRules({projectId: this.projectId, rules});
    }

}

describe('Firestore Rules', function () {

    before(async () => {
        await start();
        console.log('Starting local Firestore Emulator');
        await Utils.wait(3000);
    });

    after(async () => {
        try {
            await stop();
        } catch (ignored) {
        }
        await Utils.wait(1000);
        console.log('Stopped local Firestore Emulator');
        await Promise.all(firebase.apps().map(app => app.delete()));
    });

    describe('Unauthenticated', function () {

        it('should fail to read and write from users', async () => {
            const tc = new FirestoreRuleTest();

            // Add test data
            const profileDoc = tc.firestore.doc('users/alice');
            await profileDoc.set({});

            await tc.loadRules();

            await firebase.assertFails(profileDoc.get());
            await firebase.assertFails(profileDoc.set({}));
        });
    });

    describe('Authenticated', function () {

        it('should succeed reading own user', async () => {

            const tc = new FirestoreRuleTest('bob');

            const userDoc = tc.firestore.doc('users/bob');
            await userDoc.set({});

            await tc.loadRules();

            await firebase.assertSucceeds(userDoc.get());
        });

        it('should fail reading different user', async () => {

            const tc = new FirestoreRuleTest('bob');

            const aliceDoc = tc.firestore.doc('users/alice');
            const bobDoc = tc.firestore.doc('users/bob');
            await aliceDoc.set({});
            await bobDoc.set({});

            await tc.loadRules();

            await firebase.assertFails(aliceDoc.get());
        });

        it('should successfully read a certificate', async () => {
            const tc = new FirestoreRuleTest('bob');

            const certDoc = await tc.firestore.doc('courses/courseId/certificates/certId');
            await certDoc.set({
                userId: 'bob'
            });

            await tc.loadRules();

            await firebase.assertSucceeds(certDoc.get());
        });

        it('should fail to read a certificate from a different user', async () => {
            const tc = new FirestoreRuleTest('bob');

            const certDoc = await tc.firestore.doc('courses/courseId/certificates/certId');
            await certDoc.set({
                userId: 'alice'
            });

            await tc.loadRules();

            await firebase.assertFails(certDoc.get());
        });

    });
});

It would be amazing to have some tooling for reliable integration testing for the Firestore rules.

With the recent changes to the Firebase emulator suite, you can "wrap" commands with

firebase emulators:exec "npm test"

which will boot up the emulators (blocking until they're ready) and then run your command.

How well does that address the use-cases here?

Closing this old issue as emulators:exec is our recommended way to run tests and emulators together (as @ryanpbrewster mentioned). Thanks all for the feedback!

Was this page helpful?
0 / 5 - 0 ratings