Cannot call Firestore from Cloud Functions unit tests - firebase

Developing Google cloud functions locally.
Trying to test functions that invoke Firestore.
Here is a minimal example.
Emulators are running.
The function addMessage() works completely fine when invoked from the browser.
The function fails with error TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. when invoked from tests.
Question: Why is this error occurring and how can I invoke the Firestore function successfully from the tests?
functions/index.js:
require ('dotenv').config();
const functions = require ('firebase-functions');
const admin = require ('firebase-admin');
admin.initializeApp();
exports.addMessage = functions.https.onRequest (async (req, res) => {
const original = req.query.text;
const writeResult = await admin.firestore().collection ('messages').add ({text: original});
const docSnap = await writeResult.get();
const writtenText = docSnap.get ('text');
res.send (`Message with text: ${writtenText} added.`);
});
functions/test/index.test.js:
const admin = require ('firebase-admin');
const firebase_functions_test = require ('firebase-functions-test')({
projectId: 'my-project-id'
}, '/path/to/google-application-credentials.json');
const testFunctions = require ('../index.js');
describe ('addMessage()', () => {
it ('returns Message with text: Howdy added.', (done) => {
const req = {query: {text: 'Howdy'} };
const res = {
send: (body) => {
expect (body).toBe (`Message with text: Howdy added.`);
done();
}
};
testFunctions.addMessage (req, res);
});
});
Starting jest from functions folder:
(Among other test-related output):
FAIL test/index.test.js
● addMessage() › returns Message with text: Howdy added.
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received type object
7 | exports.addMessage = functions.https.onRequest (async (req, res) => {
8 | const original = req.query.text;
> 9 | const writeResult = await admin.firestore().collection ('messages').add ({text: original});
| ^
10 | const docSnap = await writeResult.get();
11 | const writtenText = docSnap.get ('text');
12 | res.send (`Message with text: ${writtenText} added.`);
at GrpcClient.loadProto (node_modules/google-gax/src/grpc.ts:166:23)
at new FirestoreClient (node_modules/#google-cloud/firestore/build/src/v1/firestore_client.js:118:38)
at ClientPool.Firestore._clientPool.pool_1.ClientPool [as clientFactory] (node_modules/#google-cloud/firestore/build/src/index.js:326:26)
at ClientPool.acquire (node_modules/#google-cloud/firestore/build/src/pool.js:87:35)
at ClientPool.run (node_modules/#google-cloud/firestore/build/src/pool.js:164:29)
at Firestore.request (node_modules/#google-cloud/firestore/build/src/index.js:983:33)
at WriteBatch.commit_ (node_modules/#google-cloud/firestore/build/src/write-batch.js:496:48)
Caused by: Error:
at WriteBatch.commit (node_modules/#google-cloud/firestore/build/src/write-batch.js:415:23)
at DocumentReference.create (node_modules/#google-cloud/firestore/build/src/reference.js:283:14)
at CollectionReference.add (node_modules/#google-cloud/firestore/build/src/reference.js:2011:28)
at Object.<anonymous>.exports.addMessage.functions.https.onRequest (index.js:9:71)
at Object.addMessage (node_modules/firebase-functions/lib/providers/https.js:50:16)
at Object.done (test/index.test.js:17:19)
Environment:
"node": "10"
"firebase-admin": "^8.12.1",
"firebase-functions": "^3.7.0"
"firebase-functions-test": "^0.2.1",
"jest": "^25.5.4"

Solved by adding jest.config.js to functions/:
module.exports = {
testEnvironment: 'node'
};
Solution based on this (but NB I needed to place jest.config.js in functions/, not the project root).
Also tried this but appeared to do nothing.
Tests now run perfectly except that they end with a Jest error:
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
This seems to be harmless.
Thanks #gso_gabriel for the valuable links!

Related

Cannot find module 'firebase-functions/lib/encoder' from 'node_modules/firebase-functions-test/lib/providers/firestore.js'

Hey I am trying to unit test this cloud function right here:
import { logger, region, https } from "firebase-functions/v1";
import Message from "../types/message";
const helloWorldHandler = region("europe-west1").https.onCall((_, context) => {
if (context.app == undefined) {
throw new https.HttpsError("failed-precondition", "The function must be called from an App Check verified app.");
}
logger.info("Hello logs!", { structuredData: true });
const message: Message = {
text: "Hello from Firebase!",
code: 200,
};
return message;
});
export default helloWorldHandler;
with the following test:
import * as functions from "firebase-functions-test";
import * as path from "path";
const projectConfig = {
projectId: "myproject-id",
};
const testEnv = functions(projectConfig, path.resolve("./flowus-app-dev-fb-admin-sdk-key"));
// has to be after initializing functions
import helloWorldHandler from "../src/functions/helloworld";
import Message from "../src/types/message";
describe('Testing "helloWorld"', () => {
const helloWorld = testEnv.wrap(helloWorldHandler);
it("helloWorld does work", async () => {
const data = {};
const success: Message = await helloWorld(data);
expect(success.code).toBe(200);
});
});
When I run it with yarn test I receive the following error
Cannot find module 'firebase-functions/lib/encoder' from 'node_modules/firebase-functions-test/lib/providers/firestore.js'
Even though my function does not even use firestore in the first place?
Any ideas ?
I was facing a similar issue while trying to set up a unit testing environment for firebase cloud functions.
Mainly, after following all the steps on Firebase's docs, and running npm test
I would get the following error
Error [ERR_PACKAGE_PATH_NOT_EXPORTED] Package subpath './lib/encoder'
is not defined by "exports"
After stumbling on Farid's suggestion for this problem, I realized that, for some reason, npm i firebase-functions-test does not install the latest version of the module.
Try npm i firebase-functions-test#latest.

Error when unit testing cloud function: no document found to update

I am following the firebase documentation for unit testing. I created a simple cloud function that is triggered when a firestore document is created. It turns the value in that document to uppercase. I tried the function by creating a document and it worked as expected.
I use jest for the test. When I run the test I get the following error:
NOT_FOUND: No document to update: projects/myproject/databases/(default)/documents/test/testId
10 | return admin.firestore().doc(`test/` + snap.id)
> 11 | .update({ input: input });
| ^
12 | });
I added a console.log to my function to see what data was passed by the unit-test. It passes the right data (meaning the right document id and the right value for "input".
I'm not sure what I'm missing here. It's a simple function and a simple test. I don't understand how the function could receive the right data but not found the document, or how the document is not created in firestore when the test is run.
The function:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
export const makeUpperCase = functions.firestore
.document('test/{id}')
.onCreate((snap, context) => {
const data = snap.data();
const input = data.input.toUpperCase();
console.log('new data: ' + input + ', id: ' + snap.id);
return admin.firestore().doc('test/' + snap.id)
.update({ input: input });
});
index.ts:
import * as admin from 'firebase-admin';
admin.initializeApp();
export { makeUpperCase } from './makeUpperCase';
the test (basicTest.test.ts):
import * as functions from 'firebase-functions-test';
import * as admin from 'firebase-admin';
import 'jest';
const testEnv = functions({
databaseURL: "https://mydb.firebaseio.com",
projectId: "myproject",
}, './service-account.json');
testEnv.mockConfig({});
import { makeUpperCase } from '../src/index';
describe('basicTest', () => {
let wrapped: any;
beforeAll(() => {
wrapped = testEnv.wrap(makeUpperCase);
});
test('converts input to upper case', async () => {
const path = 'test/testId';
const data = { input: 'a simple test' };
const snap = await testEnv.firestore.makeDocumentSnapshot(data, path);
await wrapped(snap);
const after = await admin.firestore().doc(path).get();
expect(after.data()!.input).toBe('A SIMPLE TEST');
});
});
The problem comes from your use of the import syntax. Unlike require, the import instructions are performed asynchronously before the script that is importing them is executed.
import * as module1 from 'module1';
console.log(module1);
import * as module2 from 'module2';
console.log(module1);
is effectively (not exactly) the same as
const module1 = require('module1');
const module2 = require('module2');
console.log(module1);
console.log(module2);
Because of this quirk, in your current code, admin.firestore() is incorrectly pointing at your live Firestore database and test.firestore is pointing at your test Firestore database. This is why the document doesn't exist, because it exists on your test database, but not your live one.
This is why the documentation you linked uses require statements, and not the import syntax.
// this is fine, as long as you don't call admin.initializeApp() before overriding the config and service account as part of test's initializer below
import * as admin from 'firebase-admin';
import 'jest';
// Override the configuration for tests
const test = require("firebase-functions-test")({
databaseURL: "https://mydb.firebaseio.com",
projectId: "myproject",
}, './service-account.json');
test.mockConfig({});
// AFTER mocking the configuration, load the original code
const { makeUpperCase } = require('../src/index');
describe('basicTest', () => {
let wrapped: any;
beforeAll(() => {
wrapped = test.wrap(makeUpperCase);
});
test('converts input to upper case', async () => {
const path = 'test/testId';
const data = { input: 'a simple test' };
const snap = await test.firestore.makeDocumentSnapshot(data, path);
await wrapped(snap);
const after = await admin.firestore().doc(path).get();
expect(after.data()!.input).toBe('A SIMPLE TEST');
});
});

How to do integration tests on Firebase Functions using local emulator?

I successfully wrote some tests for my Firebase functions, however now I want to test functions that manipulate Firestore data.
to do so I execute the following
export GOOGLE_APPLICATION_CREDENTIALS="my-project-key.json"
export FIRESTORE_EMULATOR_HOST="localhost:8080"
firebase emulators:start --import ./functions/test/fixture --project my-project
and then I run
npm run test
The test code is as follows:
const test = require("firebase-functions-test")()
const functions = require("../index")
describe("Tests", () => {
it("Do test", async () => {
const wrapped = test.wrap(functions.doTest)
const result = await wrapped({id:"1"})
})
})
The index file imported contains:
const functions = require("firebase-functions")
const admin = require("firebase-admin")
admin.initializeApp()
exports.doTest = functions.https.onCall(async (data) => {
const {id} = data
const vehicleRef = admin.firestore().collection("vehicles").doc(id)
const vehicle = await vehicleRef.get()
})
Yet every time I call a Firebase function that accesses Firestore I get the following error (in this case on vehicleRef.get()):
Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
at GoogleAuth.getApplicationDefaultAsync (/MyProject/firebase/functions/node_modules/google-auth-library/build/src/auth/googleauth.js:157:19)
at processTicksAndRejections (internal/process/task_queues.js:94:5)
at async GoogleAuth.getClient (/MyProject/firebase/functions/node_modules/google-auth-library/build/src/auth/googleauth.js:490:17)
at async GrpcClient._getCredentials (/MyProject/firebase/functions/node_modules/google-gax/build/src/grpc.js:87:24)
at async GrpcClient.createStub (/MyProject/firebase/functions/node_modules/google-gax/build/src/grpc.js:212:23)
Caused by: Error
at Firestore.getAll (/MyProject/firebase/functions/node_modules/#google-cloud/firestore/build/src/index.js:784:23)
at DocumentReference.get (/MyProject/firebase/functions/node_modules/#google-cloud/firestore/build/src/reference.js:201:32)
at Function.run (/MyProject/firebase/functions/src/tracker.js:40:28)
at wrapped (/MyProject/firebase/functions/node_modules/firebase-functions-test/lib/main.js:72:30)
at Context.<anonymous> (/MyProject/firebase/functions/test/testTracker.js:42:26)
at callFn (/MyProject/firebase/functions/node_modules/mocha/lib/runnable.js:366:21)
at Test.Runnable.run (/MyProject/firebase/functions/node_modules/mocha/lib/runnable.js:354:5)
at Runner.runTest (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:677:10)
at /MyProject/firebase/functions/node_modules/mocha/lib/runner.js:801:12
at next (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:594:14)
at /MyProject/firebase/functions/node_modules/mocha/lib/runner.js:604:7
at next (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:486:14)
at Immediate._onImmediate (/MyProject/firebase/functions/node_modules/mocha/lib/runner.js:572:5)
What am I doing wrong?
Looks like you're doing everything right, except I don't think you're initializing the initializeApp object correctly. Try this in your index:
admin.initializeApp({
projectId: 'my-project',
credential: admin.credential.applicationDefault(),
});
You'll have to add a condition to use this version based on whether or not the environment variable is set though.

Trying to connect to firebase function from React app - cors issue?

I'm creating a react application. I have code like this
async componentDidMount() {
const questions = await axios.get('getQuestions');
console.log(questions);
}
(I have a baseURL set up for axios and all, so the URL is correct)
I created a firebase function as follows (typescript)
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
admin.firestore().settings({ timestampsInSnapshots: true });
const db = admin.firestore();
exports.getQuestions = functions.https.onRequest(async (request, response) => {
const questions = [];
const querySnapshot = await db.collection('questions').get();
const documents = querySnapshot.docs;
documents.forEach(doc => {
questions.push(doc.data());
});
response.json({ questions: questions });
});
Now when I build and run firebase deploy --only functions, and then visit the url directly, everything works. I see my questions.
But in the react app, I get the following error
Access to XMLHttpRequest at '.../getQuestions' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
After some googling, I tried
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
admin.firestore().settings({ timestampsInSnapshots: true });
const db = admin.firestore();
const cors = require('cors')({ origin: true });
exports.getQuestions = functions.https.onRequest(
cors(async (request, response) => {
const questions = [];
const querySnapshot = await db.collection('questions').get();
const documents = querySnapshot.docs;
documents.forEach(doc => {
questions.push(doc.data());
});
response.json({ questions: questions });
})
);
But that just gave me an error when I ran firebase deploy --only functions
✔ functions: Finished running predeploy script. i functions:
ensuring necessary APIs are enabled... ✔ functions: all necessary
APIs are enabled i functions: preparing functions directory for
uploading...
Error: Error occurred while parsing your function triggers.
TypeError: Cannot read property 'origin' of undefined
at ...
And tbh, even if this command worked, I don't know if it is the correct solution
Got it :) I was doing something silly
import * as cors from 'cors';
const corsHandler = cors({ origin: true });
exports.getQuestions = functions.https.onRequest(async (request, response) => {
corsHandler(request, response, async () => {
const questions = [];
const querySnapshot = await db.collection('questions').get();
const documents = querySnapshot.docs;
documents.forEach(doc => {
questions.push(doc.data());
});
response.status(200).json({ questions: questions });
});
});
This answer will help someone who facing cors error.
01 - Create Firebase Function Called BtnTrigger (You can name whatever you like)
// Include Firebase Function
const functions = require('firebase-functions');
// Include Firebase Admin SDK
const admin = require('firebase-admin');
admin.initializeApp();
//cors setup include it before you do this
//run npm install cors if its not in package.json file
const cors = require('cors');
//set origin true
const corsHandler = cors({ origin: true });
//firebase function
export const BtnTrigger = functions.https.onRequest((request, response) => {
corsHandler(request, response, async () => {
//response.send("test");
response.status(200).json({ data: request.body });
});
});
Then Run firebase deploy --only functions this will create your firebase function. if you need you can check it from your Firebase Console.
02 - Create Function Trigger from your Application from your application code
i used same BtnTrigger name to understand it properly you can change variable here but httpsCallable params should same as your Firebase Function Name you created.
var BtnTrigger =firebase.functions().httpsCallable('BtnTrigger');
BtnTrigger({ key: value }).then(function(result) {
// Read result of the Cloud Function.
console.log(result.data)
// this will log what you have sent from Application BtnTrigger
// { key: value}
});
Don't Forget to import 'firebase/functions' from your Application Code

Unable to send SMS through Twilio and Google Functions

I am attempting to send a text (a one-time pass code) using Twilio, firebase and Google Functions, and using Postman.
I have run $ npm install --save twilio#3.0.0 -rc.13 in the functions directory.
When I run $ firebase deploy, it completes. But on Postman, when I do POST, Body and feed a JSON { "phone": "555-5555" }, I get an "Error: could not handle the request."
I am able to send a text in Twilio Programmable SMS from my Twilio number to an actual outside number direct to the mobile phone. I'm using live credentials for Sid and AuthToken.
Is this an issue with Twilio, Google Functions and some configurations?
Here are the logs on Functions:
// White flag sign//
Function execution took 1452 ms, finished with status: 'crash'
//Red Warning sign//
TypeError: handler is not a function
at cloudFunction (/user_code/node_modules/firebase-functions/lib/providers/https.js:26:41)
at /var/tmp/worker/worker.js:676:7
at /var/tmp/worker/worker.js:660:9
at _combinedTickCallback (internal/process/next_tick.js:73:7)
at process._tickDomainCallback (internal/process/next_tick.js:128:9)
Also, the google eslint forces consistent-return, which is why I put "return;" in the request-one-time-password.js. I cannot seem to turn it off by adding "consistent-return": 0 in eslintrc.
My code(with secret keys and phone numbers redacted):
//one-time-password/functions/service_account.js
has my keys copied and pasted.
//one-time-password/functions/twilio.js
const twilio = require('twilio');
const accountSid = 'redacted';
const authToken = 'redacted';
module.exports = new twilio.Twilio(accountSid, authToken);
//one-time-password/functions/request-one-time-password.js
const admin = require('firebase-admin');
const twilio = require('./twilio');
module.export = function(req, res) {
if(!req.body.phone) {
return res.status(422).send({ error: 'You must provide a phone number!'});
}
const phone = String(req.body.phone).replace(/[^\d]/g, '');
admin.auth().getUser(phone).then(userRecord => {
const code = Math.floor((Math.random() * 8999 + 1000));
// generate number between 1000 and 9999; drop off decimals
twilio.messages.create({
body: 'Your code is ' + code,
to: phone,
from: '+redacted'
}, (err) => {
if (err) { return res.status(422).send(err); }
admin.database().ref('users/' + phone).update({ code: code, codeValid: true }, () => {
res.send({ success: true });
})
});
return;
}).catch((err) => {
res.status(422).send({ error: err })
});
}
/////////////////////////////////
//one-time-password/functions/index.js
const admin = require('firebase-admin');
const functions = require('firebase-functions');
const createUser = require('./create_user');
const serviceAccount = require('./service_account.json')
const requestOneTimePassword = require('./request_one_time_password');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://one-time-password-650d2.firebaseio.com"
});
exports.createUser = functions.https.onRequest(createUser);
exports.requestOneTimePassword =
functions.https.onRequest(requestOneTimePassword);
You have
module.exports = new twilio.Twilio(accountSid, authToken);
on one line, and further down
module.export = function(req, res) { ... }.
Try changing export to exports.
One thing that tripped me up for a long time was how twilio sent the request body to the cloud function. It sends it in a body object so to access your request body it will look something like this
req.body.body
On top of that it passed it as a JSON string so I had to JSON.parse()
Example I got working:
export const functionName= functions.https.onRequest((req, res) => {
cors(req, res, () => {
let body = JSON.parse(req.body.body);
console.log(body);
console.log(body.service_id);
res.send();
});
});
This also may depend on the Twilio service you are using. I was using their Studio HTTP Request Module.
Hope this helps a little, not sure if it was your exact problem though :(

Resources