How to test `functions.https.onCall` firebase cloud functions locally? - firebase

I can't seem to find the solution for this in the Firebase Documentation.
I want to test my functions.https.onCall functions locally. Is it possible using the shell or somehow connect my client (firebase SDK enabled) to the local server?
I want to avoid having to deploy every time just to test a change to my onCall functions.
My code
Function :
exports.myFunction = functions.https.onCall((data, context) => {
// Do something
});
Client:
const message = { message: 'Hello.' };
firebase.functions().httpsCallable('myFunction')(message)
.then(result => {
// Do something //
})
.catch(error => {
// Error handler //
});

For locally you must call (after firebase.initializeApp)
firebase.functions().useFunctionsEmulator('http://localhost:5000')

Although the official Firebase Cloud Function docs have not yet been updated, you can now use firebase-functions-test with onCall functions.
You can see an example in their repository.
I have managed to test my TypeScript functions using jest, here is a brief example. There are some peculiarities here, like import order, so make sure to read the docs :-)
/* functions/src/test/index.test.js */
/* dependencies: Jest and jest-ts */
const admin = require("firebase-admin");
jest.mock("firebase-admin");
admin.initializeApp = jest.fn(); // stub the init (see docs)
const fft = require("firebase-functions-test")();
import * as funcs from "../index";
// myFunc is an https.onCall function
describe("test myFunc", () => {
// helper function so I can easily test different context/auth scenarios
const getContext = (uid = "test-uid", email_verified = true) => ({
auth: {
uid,
token: {
firebase: {
email_verified
}
}
}
});
const wrapped = fft.wrap(funcs.myFunc);
test("returns data on success", async () => {
const result = await wrapped(null, getContext());
expect(result).toBeTruthy();
});
test("throws when no Auth context", async () => {
await expect(wrapped(null, { auth: null })).rejects.toThrow(
"No authentication context."
);
});
});

There is a simple trick, how you can simplify onCall -function testing. Just declare the onCall function callback as a local function and test that instead:
export const _myFunction = (data, context) => { // <= call this on your unit tests
// Do something
}
exports.myFunction = functions.https.onCall(_myFunction);
Now you can variate all cases with a normal function with the input you define on your function call.

Callables are just HTTPS functions with a specific format. You can test just like a HTTPS function, except you have to write code to deliver it the protocol as defined in the documentation.

you should first check for dev environment and then point your functions to local emulator.
For JS:
//after firebase init
if (window.location.host.includes("localhost") ||
window.location.host.includes("127.0.0.1")
) {
firebase
.app()
.functions() //add location here also if you're mentioning location while invoking function()
.useFunctionsEmulator("http://localhost:5001");
}
or if you don't create instance of firebase then
//after firebase init
if (window.location.host.includes("localhost") ||
window.location.host.includes("127.0.0.1")
) {
firebase
.functions()
.useFunctionsEmulator("http://localhost:5001");
}
or when serving pages from backend (node.js):
//after firebase init
if (process.env.NODE_ENV === 'development') {
firebase.functions().useFunctionsEmulator('http://localhost:5001');
}

if you are using angularfire, add this to you app.module
{
provide: FirestoreSettingsToken,
useValue: environment.production
? undefined
: {
host: "localhost:5002",
ssl: false
}
}

Related

Firebase Cloud Function unit test HTTP onCall

I would like to write some unit tests for a bunch of cloud functions. Now I'm facing the following issue. Using the firebase-functions-test I'm somehow not able to test HTTP triggered cloud functions. Here are some of my cloud functions that i'd like to test using jest:
export cloudFunctions = {
createUserByAdmin: functions.runWith({ timeoutSeconds: 30, memory: '256MB' }).https.onCall(UserService.createUserByAdmin),
updateUserByAdmin: functions.runWith({ timeoutSeconds: 30, memory: '256MB' }).https.onCall(UserService.updateUserByAdmin),
deleteUserByAdmin: functions.runWith({ timeoutSeconds: 30, memory: '256MB' }).https.onCall(UserService.deleteUserByAdmin)
}
They are all deployed on Firebase and they work without problems. But I couldn't find a way to call the using the firebase-functions-test package. Also, there are a few examples of how to write unit tests using that package but none of them test http triggered functions.
This is my test file:
import * as functions from 'firebase-functions-test'
import * as admin from 'firebase-admin'
import * as path from 'path'
const projectConfig = {
projectId: 'test-fb',
}
const testEnv = functions(
projectConfig,
path.resolve('DO-NOT-EDIT.dev.fb-admin-sdk-key.json'),
)
describe('[Cloud Functions] User Service', () => {
let cloudFunctions
beforeAll(() => {
cloudFunctions = require('../index')
})
afterAll(() => {
// delete made accounts/entrys
})
describe('Testing "createUserByAdmin"', () => {
it('Creating User does work', () => {
expect(1).toBe(0)
})
})
})
Does someone know how to test http cloud functions? I would really appreciate an example.
Thanks!
I actually found a way on how to test HTTP Cloud Functions using firebase-functions-test it all works with a wrapper function. Take a look at this reference page. Here is some code to make things a bit more clear.
this is a snippet from one of my tests
import * as functions from 'firebase-functions-test'
import * as admin from 'firebase-admin'
import * as path from 'path'
const projectConfig = {
projectId: 'myproject-id',
}
const testEnv = functions(
projectConfig,
path.resolve('fb-admin-sdk-key.json'),
)
// has to be after initializing functions
import * as cloudFunctions from '../index'
describe('Testing "createUserByAdmin"', () => {
const createUserByAdmin = testEnv.wrap(cloudFunctions.createUserByAdmin)
it('Creating User does work', async (done) => {
const data = {
displayName: 'Jest Unit Test',
email: 'unit#domain.com',
password: 'password',
uid: null,
}
const context = {
auth: {
token: {
access: 'somestring,
},
uid: 'mockuiddddd',
},
}
await createUserByAdmin(data, context)
.then(async (createdUser: any) => {
expect(createdUser.status).toBe('OK')
done()
})
.catch((error: any) => {
fail('createUserByAdmin failed with the following ' + error)
})
})
})
You'll see that after initializing our test environment using our projectConfig and our service account key file.
const testEnv = functions(
projectConfig,
path.resolve('fb-admin-sdk-key.json'),
)
you'll just have to wrap the appropriate cloud function with the .wrap() function.
const createUserByAdmin = testEnv.wrap(cloudFunctions.createUserByAdmin)
And you can call it like every other function (keep in mind that cloud functions usually expect a data parameter (with the variables you use in your cloud function) as well as a context parameter, depending on how you handle authentication/authorization you'll have to try and error to find the right context properties your function requests.
if your writing tests for your production cloud functions make sure to clean up after running tests - such as deleting created accounts or deleting data in either firestore or realtime-database

connecting to firestore emulator with #firebase/testing

I am trying to test a firebase app locally.
I am running the test with firebase emulators:exec --only firestore 'mocha -r ts-node/register src/**/*.spec.ts
In my spec, I import #firebase/testing and setup my app and followed the directions from
https://firebase.google.com/docs/rules/unit-tests
I have a FirebaseService which is a singleton wrapper for my methods into which I inject my firebase app.
In production, I'll inject the firebase, and it gets initialized in the FirebaseService in testing, I initialize outside of the service.
The wrapper is fairly simple
export const FirebaseService = (function(): FirebaseSrvc {
let firebase;
const fbServiceObj: FirebaseSrvc = {
getInstance: (firebaseConfig, firebaseCore, initialize) => {
firebase = firebaseCore;
if (initialize && firebase.apps.length === 0) {
firebase.initializeApp(firebaseConfig);
}
return fbServiceObj;
},
createActivity: async (title: string) => {
try {
const firebaseUid = firebase.auth().currentuser.uid;
const newActivity: ActivityProps = {
title,
created_at: 123445,
created_by: firebaseUid,
public: false,
available_to: [firebaseUid],
};
console.log(' before create', newActivity);
const createResponse = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.set(newActivity);
console.log('create response', createResponse);
return true;
} catch (e) {
console.log('error creating activity', e);
}
},
getActivity: async (title: string): Promise<ActivityProps> => {
try {
const actResponse: DocumentReferenceTo<ActivityProps> = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.get();
return actResponse as ActivityProps;
} catch (e) {
console.log('error getting activity from firebase', e);
}
},
};
return fbServiceObj;
})();
The test I am attempting to run is
import * as firebase from '#firebase/testing';
import { assert } from 'chai';
import 'mocha';
import * as appConfig from '../../app-dev.json';
import { FirebaseService } from '../services/FirebaseService';
firebase.initializeTestApp({ ...appConfig.expo.extra.firebase, auth: { uid: 'random', email: 'test#test.com' } });
describe('Activity', async () => {
const fb = FirebaseService.getInstance(appConfig.expo.extra.firebase, testApp, false);
const activityData = new Activity(fb);
beforeEach(async () => await firebase.clearFirestoreData({ projectId }));
it('should create a new activity', async () => {
await activityData.set('test-activity'); // this runs FirebaseService.createActivity
const findActivity = await activityData.get('test-activity'); // this run FirebaseService.getActivity
assert(findActivity.title === 'test-activity');
});
});
When I run the test I get an error
Your API key is invalid, please check you have copied it correctly.] {
code: 'auth/invalid-api-key',
message: 'Your API key is invalid, please check you have copied it correctly.'
}
I can confirm that the API key which is passed into firebase.initializeTestApp matches the Web API Key in my firebase console.
I have also downloaded the google-services.json from my firebase console and lists
{
"api_key": [
{ "current_key": different_from_web_key}
]
}
And I have replaced my existing key with this new key, I still get the same error.
I have also tried setting up initializeTestApp({ projectId }) which is how the example from firebase docs sets it up, and I receive the same result.
I am using the same project details to run a project locally in android studio, and I am able to authenticate and write to firestore, so the API key I am using does work, but it appears to have issues being used in the test app.
This usually doesn't have a specific way to solve it. It might be that even a new copy and paste of the API key to the parameters, might make it work and the error to disappear.
I would recommend you to take a look at the following posts from the Community, that have some possible fixes for the error that you are facing.
Firebase Error: auth/invalid-api-key, Your API key is invalid, please check you have copied it correctly
Invalid API Key supplied using Firebase
In addition to that, since Firebase has free support offers, I think you reaching out to the Firebase support would help you fix this quickly. You should be able to contact directly for free.
Let me know if the information helped you!

Firebase functions - res.status is undefined

This is my code which I am executing:
exports.checkPin = functions.https.onCall(async (req, res) => {
let roomDoc = await db.collection('Rooms').where('roomNum', "==", req.roomNum).get();
if (roomDoc.exists) {
if (bcrypt.compareSync(roomDoc.data().pin, req.pin)) {
res.status(200).send("authorised");
} else {
res.status(401).send("unauthorised");
}
} else {
res.status(401).send("unauthorised");
}
});
After executing the code, it throws an error saying that res.status() is undefined
Am I doing something wrong? I want to be able to send a response HTTP code, once the firebase function has finished executing, if this isn't the right way to do it?
You are mixing up Callable Cloud Functions and HTTP Cloud Functions.
Doing res.status(XXX).send("..."); shall be done in an HTTP Cloud Function and not in a Callable one.
Also, note that roomDoc returns a QuerySnapshot and not a DocumentSnapshot.
So you should adapt your function as follows (making the assumption that the query will return only one document):
exports.checkPin = functions.https.onRequest(async (req, res) => { // <--- See here, we use onRequest and not onCall
const querySnapshot = await db.collection('Rooms').where('roomNum', "==", req.roomNum).get();
if (!querySnapshot.empty) {
const roomDoc = querySnapshot.docs[0];
if (bcrypt.compareSync(roomDoc.data().pin, req.pin)) {
res.send("authorised");
} else {
res.status(500).send("unauthorised");
}
} else {
res.status(500).send("unauthorised");
}
PS: you may watch this official video on HTTPS Cloud Functions: https://www.youtube.com/watch?v=7IkUgCLr5oA&t=1s&list=PLl-K7zZEsYLkPZHe41m4jfAxUi0JjLgSM&index=3

Using a callable function to send data back to the client from Firebase

I have created a callable Cloud Function to read data from Firebase and send back the results to the client, however, only "null" is being returned to the client.
exports.user_get = functions.https.onCall((data, context) => {
if (context.auth && data) {
return admin.firestore().doc("users/" + context.auth.uid).get()
.then(function (doc) {
return { doc.data() };
})
.catch(function (error) {
console.log(error);
return error;
})
} return
});
I just reproduced your case connecting from a Cloud Function with a Firestore database and retriving data. As I can see you are trying to access the field in a wrong way when you are using "users/" + context.auth.uid, the method can't find the field so its returning a null value.
I just followed this Quickstart using a server client library documentation to populate a Firestore database and make a Get from it with node.js.
After that i followed this Deploying from GCP Console documentation in order to deploy a HTTP triggered Cloud Function with the following function
exports.helloWorld = (req, res) => {
firestore.collection('users').get()
.then((snapshot) => {
snapshot.forEach((doc) => {
console.log(doc.id, '=>', doc.data().born);
let ans = {
date : doc.data().born
};
res.status(200).send(ans);
});
})
And this is returning the desired field.
You can take a look of my entire example code here
This is because you are making a query from a database firestore, however the cloud support team has made it very cool to protect your applications from data leakages and so in a callable function as the name suggest you can only return data you passed to the same callable function through the data parameter and nothing else. if you try to access a database i suggest you use an onRequest Function and use the endpoint to get you data. that way you not only protect your database but avoid data and memory leakage.
examples of what you can return from a callable function
exports.sayHello = functions.https.onCall((data, context) => {
const name = data.name;
console.log(`hello ${name}`);
return `It was really fun working with you ${name}`;
});
first create a function in your index.js file and accept data through the data parameter but as i said you can only return data you passed through the data parameter.
now call the function
this is in the frontend code (attach an event listener to a button or something and trigger it
/* jsut say hello from firebase */
callButton.addEventListener('click', () => {
const sayHello = firebase.functions().httpsCallable('getAllUsers');
sayHello().then(resutls => {
console.log("users >>> ", resutls);
});
});
you can get your data using an onRequest like so
/* get users */
exports.getAllUsers = functions.https.onRequest((request, response) => {
cors(request, response, () => {
const data = admin.firestore().collection("users");
const users = [];
data.get().then((snapshot) => {
snapshot.docs.forEach((doc) => {
users.push(doc.data());
});
return response.status(200).send(users);
});
});
});
using a fetch() in your frontend code to get the response of the new onRequest function you can get the endpoint to the function in your firebase console dashboard.
but not that to hit the endpoint from your frontend code you need to add cors to your firebase cloud functions to allow access to the endpoint.
you can do that by just adding this line to the top of your index.js file of the firebase functions directory
const cors = require("cors")({origin: true});

Firebase: How to run 'HTTPS callable functions' locally using Cloud Functions shell?

I couldn't find a solution for this use case in Firebase official guides.
They are HTTPS callable functions
Want to run Functions locally using Cloud Functions shell to test
Functions save received data to Firestore
The 'auth' context information is also needed
My code as below. Thanks in advance.
Function :
exports.myFunction = functions.https.onCall((data, context) => {
const id = context.auth.uid;
const message = data.message;
admin.firestore()...
// Do something with Firestore //
});
Client call :
const message = { message: 'Hello.' };
firebase.functions().httpsCallable('myFunction')(message)
.then(result => {
// Do something //
})
.catch(error => {
// Error handler //
});
There is an api exactly for this use case, see here.
I used it in javascript(Client side) as follows -
button.addEventListener('click',()=>{
//use locally deployed function
firebase.functions().useFunctionsEmulator('http://localhost:5001');
//get function reference
const sayHello = firebase.functions().httpsCallable('sayHello');
sayHello().then(result=>{
console.log(result.data);
})
})
where sayHello() is the callable firebase function.
When the client is an android emulator/device. Use 10.0.2.2 in place of localhost.
Also the code for flutter would be like so -
CloudFunctions.instance.useFunctionsEmulator(origin: 'http://10.0.2.2:5000')
.getHttpsCallable(functionName: 'sayHello')
Cloud functions have emulators for that. Check this link it can suite your case. Its not functions shell, but for testing purposes i think it can still works for you
In newer versions of firebase, this is the way:
import firebaseApp from './firebaseConfig';
import { getFunctions, httpsCallable, connectFunctionsEmulator } from 'firebase/functions';
const functions = getFunctions(firebaseApp);
export async function post(funcName, params) {
connectFunctionsEmulator(functions, 'localhost', '5001'); // DEBUG
let func = httpsCallable(functions, funcName);
let result = await func(params);
return result.data;
}

Resources