Firebase Functions does not initialize admin on different class - firebase

I have a ionic 3 app that was working fine some days ago. The app receive notifications from FCM after the trigger 'onWrite', from Realtime database.
The Firebase functions npm package was obsollet, so I updated using the following command:
npm install --save firebase-functions#latest
Now, the console shows me an warning saying that my admin is with the wrong credentials. My code has some classes and every class has a admin instance. The 'main' admin is initialized on index.js very start:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const serviceController = require('./controllers/serviceController');
exports.notifyWorks = functions.database.ref('/notificationsAll/{State_}/{notificationYear}/{notificationMonth}/{notificationId}')
.onWrite((change, context) => {
return serviceController.notifyWorks(change, admin)
});
The 'serviceController' class constructor:
class serviceController {
constructor() {
this.admin = require('firebase-admin');
this.notificationIcon = ''
this.notificationSound = "ding"
}
notifyWorks(change, context){
if (!change.after.exists()) {
return new Promise(function(resolve){
resolve(console.log("Não foi possível identificar tipo de app"))
});
} else {
const snapshot = change.after
const data = snapshot.val()
if (!data) {
return new Promise(function(resolve){
resolve(console.log("Não foi possível identificar os dados da mensagem"))
});
} else {
resolve(console.log("Here we continue with the logic"))
}
}
}
}
exports.notifyWorks = function(change, context){
const helper = new serviceController();
return helper.notifyWorks(change, context)
};
The functions package.json:
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"lint": "eslint --init .",
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"dependencies": {
"babel-eslint": "^10.0.1",
"exceljs": "^3.9.0",
"firebase-admin": "~5.12.0",
"firebase-functions": "^3.14.1",
"geolib": "^2.0.24",
"google-distance-matrix": "^1.1.1",
"moment": "^2.24.0",
"moment-range": "^4.0.2",
"request": "^2.88.0",
"xml-js": "^1.6.11"
},
"devDependencies": {
"eslint": "^4.12.0",
"eslint-plugin-promise": "^3.6.0"
},
"rules": {},
"engines": {
"node": "10"
},
"private": true
}
The ionic info output:
ionic info
Ionic:
Ionic CLI : 5.4.16 (/usr/local/lib/node_modules/ionic)
Ionic Framework : ionic-angular 3.9.2 #ionic/app-scripts : 3.2.3
Cordova:
Cordova CLI : 8.1.2 (cordova-lib#8.1.1) Cordova Platforms
: android 8.1.0 Cordova Plugins : cordova-plugin-ionic-webview
1.2.1, (and 29 other plugins)
Utility:
cordova-res (update available: 0.15.3) : 0.8.1 native-run
(update available: 1.4.0) : 1.3.0
System:
Android SDK Tools : 26.1.1 (/home/diego/Android/Sdk) NodeJS
: v14.17.1 (/home/diego/.nvm/versions/node/v14.17.1/bin/node) npm
: 6.14.13 OS : Linux 5.4
This is the warning:
Sometimes the functions works just fine:
I tried to initialize the configuration before the classes initialization, like this:
But give me another error:
Looks like the credentials are not 'global' in this new version. What can I do in this case? It's possible to use an 'global' instance? I appreciate any help. Thank you.

Ultimately, the problem you are experiencing is not the cause of an issue, but the symptom of improper Cloud Function lifecycle management. To understand what I mean, I'll give you a quick walkthrough of what is happening.
Contrary to popular belief, while admin.initializeApp(); is a synchronous line of code, it starts an asynchronous process that makes it ready for use later.
When you call initializeApp(), it parses the configuration object that you passed to it or the one that it assembles from the environment variables GOOGLE_APPLICATION_DEFAULT and FIREBASE_CONFIG which are prepopulated by Cloud Functions.
Once the configuration is parsed, it starts the process of negotiating its access tokens using the credentials from the configuration object in the background. If this process fails in any way, you will usually end up with a "app/invalid-credential" error that looks like:
Credential implementation provided to initializeApp() via the "credential" property failed to fetch a valid Google OAuth2 access token with the following error: ...
When a Cloud Function completes its body of work, it is marked inactive, any resources are severely throttled and any network requests may be disconnected or blocked. The local Cloud Functions emulators don't simulate this behaviour so you should make sure you are completing all the work your Cloud Function needs to do before saying your function has completed (by chaining Promises properly or only calling res.end()/res.send()/etc once everything has finished).
A tell-tale sign that this is occurring is if you see this log message before other messages:
Function execution took ... ms, finished with ...
Importantly, when in this "inactive" state, even the requests used to negotiate the Admin SDK's access token will be interrupted which will lead to a "app/invalid-credential" error.
How to fix
You need to work through your notifyWorks function and look for any function calls that returns a Promise that you don't use await with or return to the caller.
To start out, here are a few things that I can see in the code you shared.
This Promise constructor call is confusing (as console.log() returns void (undefined)):
return new Promise(function(resolve) {
resolve(console.log("Não foi possível identificar tipo de app"))
});
should be rewritten as:
console.log("Não foi possível identificar tipo de app")
return Promise.resolve();
or (if inside a async function):
console.log("Não foi possível identificar tipo de app")
return;
Next, you make use of else statements unnecessarily. In your code, you use the following structure:
if (condition) {
return "not-exists";
} else {
if (condition) {
return "falsy-data";
} else {
return "done-stuff";
}
}
this can be flattened to just:
if (condition) {
return "not-exists";
}
if (condition) {
return "falsy-data";
}
return "done-stuff";
If you are checking change.after.exists(), there is no need to check if data is falsy if you are only checking if it is null or not.
function notifyWorks(change, context)
if (!change.after.exists()) {
console.log("Os dados foram excluídos, abortados.")
return;
}
// data won't be `null`
const data = change.after.val();
console.log("Here we continue with the logic")
// do stuff
Next, in your HTTP Event handler, you incorrectly (based on the variable names) pass through admin as context instead of context.
exports.notifyWorks = functions.database.ref('/notificationsAll/{State_}/{notificationYear}/{notificationMonth}/{notificationId}')
.onWrite((change, context) => {
return serviceController.notifyWorks(change, admin)
});
should be
exports.notifyWorks = functions.database.ref('/notificationsAll/{State_}/{notificationYear}/{notificationMonth}/{notificationId}')
.onWrite((change, context) => {
return serviceController.notifyWorks(change, context)
});
or alternatively it can be written as
exports.notifyWorks = functions.database.ref('/notificationsAll/{State_}/{notificationYear}/{notificationMonth}/{notificationId}')
.onWrite(serviceController.notifyWorks);

Related

Why can't I access .env variables when unit testing Firebase (Cloud) Functions with firebase-functions-test, mocha, and Emulator Suite?

I have a Firebase Project using Functions, Storage, and Auth that I would like to do some rudimentary unit testing using mocha and the Emulator Suite (already working for local development).
I've read the documentation front to back several times and I don't seem to understand how to set this up. My specific challenge currently is that I cannot seem to get a simple function that returns a defined environment variable to return properly due to the environment variables not loading.
My setup is as follows:
functions/index.js
const functions = require("firebase-functions")
const admin = require('firebase-admin')
admin.initializeApp()
const { getFunctions } = require('firebase-admin/functions')
...
exports.requiresEnvVariable = functions.https.onCall((data, context) => {
return process.env.MYENV_VARIABLE || ':('
})
functions/tests/test.spec.js
const { expect } = require("chai")
const sinon = require("sinon")
const admin = require("firebase-admin")
console.log(process.env.GCLOUD_PROJECT)
const test = require("firebase-functions-test")({
projectId: process.env.GCLOUD_PROJECT
}, 'serviceAccount.json')
// I've played with calling this here and stubbing, asmentioned in the docs. Nothing seems to work.
//admin.initializeApp()
//adminInitStub = sinon.stub(admin, 'initializeApp')
const myFunctions = require("../index")
describe("Unit tests", () => {
it("A good and cool test description", async () => {
const wrapped = test.wrap(myFunctions.getTToken)
const data = {}
const result = await wrapped(data)
expect('yayitworked').to.eql(result)
})
})
functions/package.json
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log",
"tests": "mocha --exit tests/*.spec.js"
},
"engines": {
"node": "18"
},
"main": "index.js",
"dependencies": {
"axios": "^1.1.3",
"cors": "^2.8.5",
"firebase-admin": "^11.4.1",
"firebase-functions": "^4.1.1",
},
"devDependencies": {
"chai": "^4.3.7",
"firebase-functions-test": "^3.0.0",
"mocha": "^10.2.0",
"sinon": "^15.0.1"
},
"private": true
}
functions/.env
MYENV_VARIABLE=yayitworked
And I run from the functions folder:
firebase emulators:exec 'npm run tests'
giving AssertionError: expected ':(' to deeply equal 'yayitworked'
Why can I not access the environment variables?
Short answer is that firebase-functions-test package doesn't load environment variables stored in .env file. You'd need to load the .env file manually as part of your mocha test setup by using packages like https://www.npmjs.com/package/dotenv.
Longer answer is that the setup described in the question - combining the Firebase Emulator and firebase-functions-test package - probably isn't working as you expect them to work.
When running
firebase emulators:exec 'npm run tests'
we get 2 separate instances of requiresEnvVariable function:
One spun up by the Firebase Emulator. This function would have loaded the .env variables because Firebase Emulator includes logic to load environment variables in appropriate .env files before spinning up a function.
One spun up by your mocha test. This "function" is really a plain JS function and will inherit environment variables in the parent process.
Tests written using firebase-functions-test are testing (2) function, not (1). In order to invoke the function (1) spun up by the Firebase Emulator, you'd need to make HTTP request to the URL exposed by the Firebase Emulator (see sample code)
(Side note: firebase-functions-test package was released years before Firebase Emulator so isn't "Firebase emulator-aware" so to speak.)

Error on having Vitest and Firestore working inside a Sveltekit project

To be simple, having one of the 3 tests writtens in this file (npm run test) will solve my issue:
https://github.com/regnou/vitest_firebase_demo/blob/main/src/lib/firebase/firestore.test.ts
Update -- 7th June [Fail with vitest]
I am using this starter : https://github.com/jmagrippis/with-svelte
I want to do some unit testings with Firestore and Vitest.
I actually have this error (the test will fail Timeouting):
[FirebaseError: Expected first argument to collection() to be a CollectionReference, a DocumentReference or FirebaseFirestore] {
: 'invalid-argument',
customData: undefined,
toString: [Function (anonymous)]
}
Update -- 8th June [Demo-project to have you reproduce the issue -- Fail with vitest]
I have created a smaller project (that keep the same amount of tecnologies, aka the same package.json) to reproduce the error:
https://github.com/regnou/vitest_firebase_demo
Error: I have a Timeout.
It seems that the Firebase SDK initializes, but operations such as setDoc, addDoc... does not work.
Here is the file where I want my tests to work:
https://github.com/regnou/vitest_firebase_demo/blob/main/src/lib/firebase/firestore.test.ts
If the unit tests succeed to executes inside this demo-project, it should also work on original-project
Update -- 10th June [Fail with mocha]
I have installed mocha (to do some tests instead of vite), and followed this file:
https://github.com/firebase/quickstart-testing/blob/master/unit-test-security-rules-v9/test/firestore.spec.js
I have used also the same dependencies as the demo mentioned above:
"#/rules-unit-testing": "^2.0.1"
"firebase": "^9.1.0"
"mocha": "^8.4.0"
But it also fails when executing the tests in my project:
FirebaseError: Expected first argument to collection() to be a CollectionReference, a DocumentReference or FirebaseFirestore
Update -- 11th June [Fail with vitest]
What I have undestood is that:
SecurityRules testing does not works with IMPORTS (just REQUIRE)
Firebase SDK9 works with mocha, but not with vitest.
Here is the same problem, that I present in a shorter way: Do you have a simple project example that run some unit tests with Firestore SDK 9 and Vitest?
Vitest and testing firebase security rules works out of the box :)
import {
assertFails,
initializeTestEnvironment,
} from "#firebase/rules-unit-testing";
import { test } from "vitest";
import fs from "fs";
import * as firstore from "firebase/firestore";
type Interview = {
transcript?: string;
feedback?: string;
error?: string;
userID: string;
question: string;
questionID: string;
audioID: string;
extention: string;
};
/**
* NOTE: The firebase emulator must be running for the test to work. I recomend putting you rules test in a different folder
* from the rest of your code and use `vitest --dir path-to-test-dir` to seperate them.
* you can use the `firebase emulators:exec --only firestore` to create a firestore instance before running the tests and
* it will also shut down automatically,
*
*/
let testEnv = await initializeTestEnvironment({
projectId: "stst-et-interviewer-dev",
// Specifing the hub allows firebase to automagically find all other emulators
hub: {
host: "localhost",
port: 4400,
},
firestore: {
rules: fs.readFileSync("rules/firestore.rules", "utf8"),
},
});
test(" not allow you to create a document if you are not authenticated", async () => {
const rouge = testEnv.unauthenticatedContext();
const doc = firstore.doc(rouge.firestore(), "interviews/my-interview");
const data: Interview = {
userID: "I have no id",
question: "My question",
questionID: "A question id",
audioID: "lol-an-audio-id",
extention: ".flac",
};
await assertFails(firstore.setDoc(doc, data));
});

Nuxt plugin not available in Vuex's 'this' in Firebase production (ERROR: this.$myPlugin is not a function)

I'm trying to upload my nuxt app to Firebase as cloud function. The problem is that in the nuxtServerInit action I'm trying to call a plugin function, which apparently is not yet defined at that moment, because an error is thrown: (ERROR: this.$myPlugin is not a function). The code works in dev mode, it's just after upload to Firebase it fails.
The setup is as follows:
myPlugin.js
let env, auth, app, $store;
export default (context, inject) => {
env = context.app.context.env;
auth = context.app.$fire.auth;
app = context.app;
$store = context.store;
inject('myPlugin', myPlugin);
};
async function myPlugin(...) {... }
nuxt.config.js
plugins: [
{ src: '~/plugins/myPlugin', mode: 'all' }, // with no mode specified it fails too
],
vuex index.js
export const actions = {
async nuxtServerInit({ dispatch, commit }, { req }) {
const tl = await dispatch("initAction");
return tl;
}
}
vuex someModule.js
const actions = {
initAction({ commit }) {
return this.$myPlugin(...).then(...) // this line throws '$myPlugin is not a function' error
}
}
What can be the reason for the different behaviour in dev and in prod modes and how could I fix the problem?
UPDATE:
After further testing I established that the problem is not caused by the nuxtServerInit timing. I moved the call of the initAction from nuxtServerInit to a page's created hook. However the same error appears: this.$query is not a function.
The problem occured, because js files were not getting fully loaded due to CORB errors caused by incorrect configuration. Details described in this question.

Firebase Functions Backend Error: Cannot Import Library

Yesterday Night I deployed my functions and the deployment was successfull without any errors.
But when i tried to execute/call the function it throwed following error and logged in firebase console :
{ Error: Failed to import the Cloud Storage client library for Node.js. Make sure to install the "#google-cloud/storage" npm package. Original error: SyntaxError: Unexpected token {
at new FirebaseError (/srv/node_modules/firebase-admin/lib/utils/error.js:43:28)
at new Storage (/srv/node_modules/firebase-admin/lib/storage/storage.js:65:19)
at /srv/node_modules/firebase-admin/lib/firebase-app.js:255:20
at FirebaseApp.ensureService_ (/srv/node_modules/firebase-admin/lib/firebase-app.js:376:23)
at FirebaseApp.storage (/srv/node_modules/firebase-admin/lib/firebase-app.js:253:21)
at FirebaseNamespace.fn (/srv/node_modules/firebase-admin/lib/firebase-namespace.js:292:45)
at Object.exports.processCard (/srv/Files/process.js:157:24)
at Busboy.bus.on (/srv/index.js:44:13)
at emitNone (events.js:106:13)
at Busboy.emit (events.js:208:7)
errorInfo:
{ code: 'storage/missing-dependencies',
message: 'Failed to import the Cloud Storage client library for Node.js. Make sure to install the "#google-cloud/storage" npm package. Original error: SyntaxError: Unexpected token {' } }
after that i tried on more time ,to check if the error has gone, but then it throwed following:
Error: The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.
at FirebaseAppError.FirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:43:28)
at FirebaseAppError.PrefixedFirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:89:28)
at new FirebaseAppError (/srv/node_modules/firebase-admin/lib/utils/error.js:124:28)
at FirebaseNamespaceInternals.initializeApp (/srv/node_modules/firebase-admin/lib/firebase-namespace.js:68:23)
at FirebaseNamespace.initializeApp (/srv/node_modules/firebase-admin/lib/firebase-namespace.js:423:30)
at Object.exports.processCard (/srv/Files/process.js:140:15)
at Busboy.bus.on (/srv/index.js:44:13)
at emitNone (events.js:106:13)
at Busboy.emit (events.js:208:7)
at Busboy.emit (/srv/node_modules/busboy/lib/main.js:37:33)
errorInfo:
{ code: 'app/duplicate-app',
message: 'The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.' },
codePrefix: 'app' }
I executed the function locally and everything was good function executed without any errors.
The weired part is that i never actully used google-storage library i use firebase-admin sdk to generate a signedURL and this is the function of generation of singedURL:
admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "urlToDatabase"
});
function getSignedUrl(file) {
console.time('URLGenerated ');
let options = { action: 'read', expires: Date.now() + 5 * 60 * 1000 }; // 5min Expiration Time
let bucketFileName = path.basename(file);
return bucket.upload(file, { destination: `public/${bucketFileName}`})
.then(() => {
return bucket.file(`public/${bucketFileName}`).getSignedUrl(options)
.then((urls) => {
fs.unlinkSync(file);
console.timeEnd('URLGenerated ');
return urls[0];
})
.catch((e) => {
console.log('Link Generation Error' + e);
});
}).catch((e) => console.log(e));
}
In the second error it says that i've initialized admin twice but i don't think i've done that. I double checked evything and also tried emulator as i mentioned before.
and this is package.json:
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "8"
},
"main": "index.js",
"dependencies": {
"#google-cloud/storage": "^5.3.0",
"busboy": "^0.3.1",
"docx": "^5.3.0",
"firebase-admin": "^9.2.0",
"firebase-functions": "^3.11.0",
"gs4fb": "^1.1.0"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0"
},
"private": true
}
It is 20 Hours now and my function is down. The functions is heart of my website.
Where is the problem ?
I've updated to latest version of firebase-admin, firebase-functions library and firebaseCLI not because i needed just because it was warning me everytime.
Latest version of firebase-Admin was causing the error. Rolled back to 8.13 and everything was on track.
Seriously Never Update to latest if NOT REQUIRED.

Firebase Cloud Functions "admin.messaging(...).send is not a function"

I have a function in Firebase Functions service that send any FCM.
I would to use admin.messaging().send() function, like this reference guide, but I got this error while function is triggered, not during deploy:
TypeError: admin.messaging(...).send is not a function
at exports.sendChatNotification.functions.database.ref.onCreate.event (/user_code/lib/index.js:113:30)
at Object.<anonymous> (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:59:27)
at next (native)
at /user_code/node_modules/firebase-functions/lib/cloud-functions.js:28:71
at __awaiter (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:24:12)
at cloudFunction (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:53:36)
at /var/tmp/worker/worker.js:700:26
at process._tickDomainCallback (internal/process/next_tick.js:135:7)
I can see this error in Functions->Log inside Firebase console.
That's my function code:
exports.sendChatNotification = functions.database.ref('/messages').onCreate(event => {
var message = {
data: {
title: 'title',
body: 'body',
},
apns: {
header: {
'apns-priority': '10',
'apns-expiration':'0'
},
payload: {
aps: {
sound: 'default',
'content-available':'1'
}
}
},
android: {
ttl: 60*1000,
priority: 'high'
},
topic: 'mytopic'
};
return admin.messaging().send(message);
});
If I use admin.messaging().sendToTopic() and (changing the message structure) it works fine. It seems Firebase doesn't support its own API.
I deploy this using Firebase tools, with command line "firebase deploy".
I have updated firebase-tools and firebase-functions and firebase-admin in my functions project.
The send() function was added in firebase-admin 5.9.0. If you want to use it, you should run npm install firebase-admin#latest in your functions folder to install the latest version. At the time of this writing, the latest version is 5.9.1.

Resources