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

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');
});
});

Related

Is there any downsides of requiring the files inside firebase cloud functions and initializingApp inside each of them?

I am using the cloud functions as following:
index.js (current)
exports.addVehicle = functions.https.onRequest(async (req, res) => {
cors(req, res, async () => {
await require("./src/vehicles/addVehicle").addVehicle(req, res);
});
});
addVehicle.js (current)
const admin = require("firebase-admin");
const Vehicle = require("../../models/Vehicle");
const app = admin.initializeApp();
const db = app.firestore();
exports.addVehicle = async (req, res) => {
try{
const vehicleInfo = new Vehicle(req.body);
const addedVehicle = await db.collection("vehicles").add(vehicleInfo);
console.log(addedVehicle);
res.json({data: "Succesfully added vehicle"});
}
catch(err){
if(err){
res.json(err);
}
}
};
But before I was using it like
index.js (previous)
const app = admin.initializeApp();
const db = app.firestore();
const { addVehicle } = require("./src/vehicles/addVehicle");
exports.addVehicle = functions.https.onRequest(async (req, res) => {
cors(req, res, async () => {
await addVehicle(req, res, db);
});
});
addVehicle.js (previous)
exports.addVehicle = async (req, res) => {
try{
const vehicleInfo = new Vehicle(req.body);
const addedVehicle = await db.collection("vehicles").add(vehicleInfo);
console.log(addedVehicle);
res.json({data: "Succesfully added vehicle"});
}
catch(err){
if(err){
res.json(err);
}
}
};
To summarize,previously, I was initializing the app inside index.js and passing the db as parameter to functions that I directly require and invoke. But now, I am not initializing the app in the index file, rather I initializeApp in every seperate cloud function itself, and also I do not require the files beforehand and invoke but rather both require and invoke them inside (firebase does not allow to initializeApp in different files with the previous method but when i require them inside it allows me to initializeApp in multiple different files).
The current version seems much more organized and clean, but my question is that if there is any other down or upsides of calling initializeApp multiple times across cloud functions ? Also I wonder why before it was not allowing to initializeApp in different files but now when I require it inside function directly, it allows?
You only need to call initializeApp one time globally, and then it will be initialized for all of your functions. So this is the accepted way of doing so:
// index.js
const admin = require("firebase-admin")
// Other imports
admin.initializeApp()
// Your exports
With this, you can now require("firebase-admin") in other files and your default app will already be initialized. And you don't have to pass db around. So your old addVehicle.js would be generally accepted and look like this:
// addVehicle.js
const admin = require("firebase-admin")
export default async function addVehicle(req, res) {
const db = admin.firestore()
try{
const vehicleInfo = new Vehicle(req.body);
const addedVehicle = await db.collection("vehicles").add(vehicleInfo);
console.log(addedVehicle);
res.json({data: "Succesfully added vehicle"});
}
catch(err){
if(err){
res.json(err);
}
}
}
And then back in your index, you could import addVehicle.js like before and export it as a https function.
// index.js
const admin = require("firebase-admin")
const addVehicle = require("./vehicles/addVehicle")
// Other imports
admin.initializeApp()
exports.addVehicle = functions.https.onRequest(addVehicle);
// Other exports
The downside to how you're doing it not in the new way - requiring the files in the body of the function - is that you're making the function work harder by requiring the files every time the function is invoked instead of one time when your cloud function runs. Because your function's (and firebase's) dependency on your addVehicle file never changes, you only need to load it once at the beginning of your functions index.
There's a time and place to import inline like you're doing, but there's no benefit and some performance issues doing it how you are doing it in the new way.

Read from firebase storage and write to firestore using firebase functions

I had tried this typescript code 👇
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import serviceAccount from "/Users/300041370/Downloads/serviceKey.json";
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
const buckObj = functions.storage.bucket("myBucket").object();
export const onWikiWrite = buckObj.onFinalize(async (object) => {
const filePath = object.name ?? "test.json";
const bucket = admin.storage().bucket("myBucket");
bucket.file(filePath).download().then((data) => {
const contents = data[0];
data = {"key": "value"};
const doc = admin.firestore().collection("myCollection").doc();
doc.set(data);
});
});
but this gave me following error
"status":{"code":7,"message":"Insufficient permissions to (re)configure a trigger (permission denied for bucket myBucket). Please, give owner permissions to the editor role of the bucket and try again.
I had asked this question here but it got closed as duplicate of this question. It basically said, storage.bucket("myBucket") feature is not supported and that I'll have to instead use match for limiting this operation to files in this specific bucket/folder. Hence, I tried this 👇
const buckObj = functions.storage.object();
export const onWikiWrite = buckObj.onFinalize(async (object) => {
if (object.name.match(/myBucket\//)) {
const fileBucket = object.bucket;
const filePath = object.name;
const bucket = admin.storage().bucket(fileBucket);
bucket.file(filePath).download().then((data) => {
const contents = data[0];
const doc = admin.firestore().collection("myCollection").doc();
const data = {content: contents}
doc.set(data);
});
}
});
I am still facing the same issue. I'll repeat that here:
"status":{"code":7,"message":"Insufficient permissions to (re)configure a trigger (permission denied for bucket myBucket). Please, give owner permissions to the editor role of the bucket and try again.
Since version 1.0 of the Firebase SDK for Cloud Functions, firebase-admin shall be initialized without any parameters within the Cloud Functions runtime.
The following should work (I've removed the check on filePath):
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
export const onWikiWrite = functions.storage
.object()
.onFinalize(async (object) => {
const fileBucket = object.bucket;
const filePath = object.name;
const bucket = admin.storage().bucket(fileBucket);
return bucket
.file(filePath)
.download()
.then((data) => {
const contents = data[0];
return admin
.firestore()
.collection('myCollection')
.add({ content: contents });
});
});
Note that we return the chain of promises returned by the asynchronous Firebase methods. It is key, in a Cloud Function which performs asynchronous processing (also known as "background functions") to return a JavaScript promise when all the asynchronous processing is complete.
We also use the add() method instead of doing doc().set().
Finally, when checking the value of the filePath, be aware of the fact that there is actually no concept of folder or subdirectory in Cloud Storage (See this answer).

Cloud Functions, Firestore trigger, recursive delete not working

Currently, I`m trying to delete all the nested hirachy datas when some documents deleted using Firestore Trigger
and my code and error code are below
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
}
);
exports.firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (change: any, context: any) => {
const path = change.path;
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and the Error code is below
FirebaseError: Must specify a path.
at Object.reject (/workspace/node_modules/firebase-tools/lib/utils.js:122:27)
why it shows error code? I do not want "callable functions" because of security
Don't ever use any when writing in TypeScript as you essentially "switch off" TypeScript and lose valuable type information such as why your code isn't working.
If you use the following lines, the type of change (which should be snapshot) and context are automatically set as QueryDocumentSnapshot and EventContext respectively as defined by the onDelete method.
exports.firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (snapshot, context) => {
});
Note: A QueryDocumentSnapshot is like a regular DocumentSnapshot, but it is guaranteed to exist (snap.exists always returns true and snap.data() will never return undefined).
Now that the type is restored to snapshot, you'll see that you can't read a property called path from it as it's not set. In your current code, path will just be set to undefined, which leads to the error saying you need to specify a path.
const path = snapshot.path; // ✖ Property 'path' does not exist on type 'QueryDocumentSnapshot'. ts(2339)
This is because path is a property of DocumentReference. To get the snapshot's underlying DocumentReference, you access it's ref property.
const path = snapshot.ref.path; // ✔
Next, in Node 10+ Cloud Functions, the list of available environment variables was changed and process.env.GCLOUD_PROJECT was removed. To get the current project ID, you can use either
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
or
const defaultApp = admin.intializeApp();
const PROJECT_ID = defaultApp.options.projectId;
This then makes your (modernized) code:
import * as admin from 'firebase-admin';
import * as firebase_tools from 'firebase-tools';
import * as functions from 'firebase-functions';
const PROJECT_ID = JSON.parse(process.env.FIREBASE_CONFIG).projectId;
admin.initializeApp(); // config is automatically filled if left empty
// although it isn't needed here
export const firestore_delete_trigger_test = functions.firestore
.document('collection1/{docs1}/collection2/{docs2}')
.onDelete(async (snapshot, context) => {
const path = snapshot.ref.path;
await firebase_tools.firestore
.delete(path, {
project: PROJECT_ID,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});

Firebase doesn't deploy my function, says it isn't a cloud function

This is my function, getAccountType
import { firebaseAdmin } from './index';
export const getAccountType = async (data: any, context: any) => {
const uid = context.auth.uid;
try{
const snapshot = await firebaseAdmin.firestore().collection('users').doc(uid).get();
if(snapshot.exists){
const data = snapshot.data() as { accountType : 'youth' | 'prof', firstName : string, isProfileComplete : boolean};
return data;
}
else{
return { message : "User does not exist"};
};
} catch(e){
return { message : `Failed to get user data ${e}` };
};
};
And this is my index.ts file
import * as functions from "firebase-functions";
import * as admin from 'firebase-admin';
import { helloWorld } from "./helloWorld";
import { getAccountType } from "./getAccountType";
export const firebaseAdmin = admin.initializeApp();
exports.helloWorld = functions.https.onCall(helloWorld)
exports.getAccountType = functions.https.onCall(getAccountType)
And here is the error I receive
i functions: preparing functions directory for uploading...
Error: Error occurred while parsing your function triggers. Please ensure that index.js only exports cloud functions.
The HelloWorld function deploys just fine, but for some reason firebase thinks getAccountType is not a cloud function
I was able to deploy your code on Firebase Cloud Functions by making some adjustments. The error is saying that you can only export functions. You cannot export declarations such as const.
To fix the error, remove firebaseAdmin on index.ts.
index.ts
import * as functions from "firebase-functions";
import { getAccountType } from "./getAccountType";
exports.getAccountType = functions.https.onCall(getAccountType)
getAccountType.ts
import * as admin from 'firebase-admin'
export const getAccountType = async (data: any, context: any) => {
// Your code logic
...
};

FIREBASE FATAL ERROR: Database initialized multiple times

I have multiple database instances in my firebase app. I am trying to write into three database instances in firebase cloud functions. My understanding by following this document is no need to initialize multiple apps for each database instance. We can initialize one and pass in the database url. As a side note, I have another function with similar kind of functionality where I have trigger event in one database and write data to other database instance and it works fine.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
const app = admin.app();
export const onStart =
functions.database.instance('my-db-1')
.ref('path')
.onCreate(async (snapshot, context) => {
return await onCreate('my-db-1',snapshot,context);
});
export const onStartDb01 = functions.database.instance('my-db-2')
.ref('path')
.onCreate(async (snapshot, context) => {
return await onCreate('my-db-2', snapshot, context);
});
async function onCreate(dbInstance: string, snapshot:
functions.database.DataSnapshot, context: functions.EventContext):
Promise<any> {
const defaultDb = app.database(defaultDbUrl);
const actvDb = app.database(actvDbUrl);
await defaultDb.ref('path')
.once("value")
.then(snap => {
const val = snap.val();
---do something and write back---
});
await actvDb.ref('path')
.once("value")
.then(snap => {
const val = snap.val();
---do something and write back---
});
return true;
}
But when a db event is fired, it logs the error as below
Error: FIREBASE FATAL ERROR: Database initialized multiple times. Please make sure the format of the database URL matches with each database() call.
You'll need to initialize a separate app() for each database instance.
Based on Doug's answer here that should be something like this:
const app1 = admin.initializeApp(functions.config().firebase)
const app2 = admin.initializeApp(functions.config().firebase)
And then:
const defaultDb = app1.database(defaultDbUrl);
const actvDb = app2.database(actvDbUrl);

Resources