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?