8

I'd like to write some integration tests (using jest, although that probably isn't very important) that test an "HTTPS callable" Firebase Cloud Function that writes to Cloud Firestore. Those writes in turn trigger other Cloud Functions. I'd like to do this using the emulator suite, i.e. not using a remote Firebase project, nor by mocking out the whole Firebase machinery. I would like to be able to manage fixture data in the Firestore.

The interaction between the emulators, @firebase/testing and firebase-functions-test is not at all obvious to me.

I have helpers like this that I use to manage test fixtures for client-side functions or helper functions (used in Cloud Functions) using the admin SDK:

const firebase = require('@firebase/testing');
const fs = require('fs');

/**
 * Set up mock app. `auth` can be an object like `{uid: "123"}` to simulate
 * authentication. `data` is used to set up some mock data ahead of time:
 * 
 * 
 * data={
 *   'collection123/doc123': {
 *     foo: 'bar'
 *   },
 *   'collection123/doc321': {
 *     foo: 'baz'
 *   }
 * }
 *
 */
exports.setup = async (auth=null, data=null, rulesFile='firestore.rules') => {

  const projectId = `rules-spec-${Date.now()}`;

  const app = firebase.initializeTestApp({
    projectId,
    auth
  });

  const adminApp = firebase.initializeAdminApp({
    projectId
  });

  const db = app.firestore();
  const adminDb = adminApp.firestore();

  // Write mock documents before rules
  if (data) {
    for (const key in data) {
      const ref = adminDb.doc(key);
      await ref.set(data[key]);
    }
  }

  // Load rules
  await firebase.loadFirestoreRules({
    projectId,
    rules: fs.readFileSync(rulesFile, 'utf8')
  });

  return db;
};

exports.setupAdmin = async (data=null, rulesFile='firestore.rules') => {
  const projectId = `rules-spec-${Date.now()}`;

  const adminApp = firebase.initializeAdminApp({
    projectId
  });

  const adminDb = adminApp.firestore();

  // Write mock documents before rules
  if (data) {
    for (const key in data) {
      const ref = adminDb.doc(key);
      await ref.set(data[key]);
    }
  }

  return adminDb;
};

tests then look like this:

test('verifyAndGetProject', async () => {
  const db = await setupAdmin({
    '/collection/foo': { foo: 1, bar: 2 }
  });

  await incrementBar(db, 'foo');
  const fooDoc = await db.doc('/collection/bar').get()).data();
  expect(fooDoc.bar).toEqual(3);
});

This works fine. However, even if both the Firestore and Functions emulators are running, Firestore-triggered-functions (e.g. onChange(...)) don't run, and if I try to call an https-callable function (e.g. firebase.functions().httpsCallable(...)(...)) I get a not-found error like:

    not-found

      at new HttpsErrorImpl (../node_modules/@firebase/testing/node_modules/@firebase/functions/src/api/error.ts:66:5)
      at _errorForResponse (../node_modules/@firebase/testing/node_modules/@firebase/functions/src/api/error.ts:175:10)
      at Service.<anonymous> (../node_modules/@firebase/testing/node_modules/@firebase/functions/src/api/service.ts:231:19)
      at step (../node_modules/tslib/tslib.js:139:27)
      at Object.next (../node_modules/tslib/tslib.js:120:57)
      at fulfilled (../node_modules/tslib/tslib.js:110:62)
          at runMicrotasks (<anonymous>)

What is the correct way to wire the SDK in the integration test up so it calls the local functions emulator, and to ensure that the functions emulator and firestore emulator both talk to each other during the test?

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
optilude
  • 3,538
  • 3
  • 22
  • 25

0 Answers0