Firebase cloud function https.onRequest inside a wrapper function - javascript

I am using Firebase cloud functions (node.js) for my app and I want to create a shared wrapper for all of my endpoints that will handle errors and have some checks.
Is it possible to create a wrapper function for the cloud functions and then expose it as a new endpoint?
Example:
// normal function that works
const helloWorld = functions.https.onRequest((req, res) => {
functions.logger.log("hello world!");
return res.end();
})
// the wrapper function that handles errors and shared logic
const functionsWrapper = async ({allowedMethods, handler}) =>
functions.https.onRequest(async (req, res) => {
try {
if (allowedMethods) {
if (!allowedMethods.includes(req.method.toLowerCase())) {
return res.status(405).end();
}
}
const result = await handler;
return res.json(result);
} catch (error) {
//handling errors here
}
}
);
// the function I want to wrap and then expose as an endpoint
const anotherFunc = functionsWrapper({
allowedMethods: ['get'],
async handler() {
return {message: 'I am inside functionsWrapper'}
}
})
// exposing the functions
module.exports = {
helloWorld, // helloWorld can be called and gets an endpoint
anotherFunc // anotherFunc does not get an endpoint
}
I think that Firebase finds where functions.https.onRequest is being exported and then exposes it as an endpoint but how can I expose 'anotherFunc'?
Or in short how to expose:
const endpoint = () => functions.htpps.onRequest((req, res) => {})
Thanks!

Related

How to create Cloud Function REST API endpoints with the read data from FIrestore

I fairly new to Cloud Functions. I want to do following
Read string data from Firestore when new document created
Use the string to create REST endpoint such as www.helloworld.com/\<ReadStringData>
Following is my code
exports.createEndPoint = functions.firestore .document("test/{DocID}")
.onCreate((snap, context) =\> {
console.log("createEndPoint function triggered");
const newValue = snap.data();
console.log(newValue);
const newEndPoint= newValue.endpoint;
console.log(newEndPoint);
return new Promise((resolve, reject) => {
console.log(`/${newEndPoint}`);
app.get(`/${newEndPoint}`, (req, res) => {
console.log("app.get function triggered");
(async () => {
try {
console.log("app.get try block triggered");
console.log(response);
resolve();
} catch (error) {
console.log("app.get catch block triggered");
console.log(error);
reject(error);
}
})();
});
});
});
exports.app = functions.https.onRequest(app);
However, the problem is www.helloworld.com/newEndPoint is never created. When I send the GET request, it times out.
This what the log shows.
Logs
I tried taking the app.get out of createEndPoint function. It didn't work because app.get never gets the newEndpoint.
let newEndPoint= "";
exports.createEndPoint = functions.firestore
.document("test/{DocID}")
.onCreate((snap, context) =\> {
console.log("createEndPoint function triggered");
const newValue = snap.data();
console.log(newValue);
newEndPoint= newValue.endpoint;
console.log(newEndPoint);
});
app.get(`/${newEndPoint}`, (req, res) =\> {
console.log("app.get function triggered");
try {
console.log("app.get try block triggered");
console.log(response);
return res.status(200).send();
} catch (error) {
console.log("app.get catch block triggered\`");
console.log(error);
return res.status(500).send(error);
}
})();
});
exports.app = functions.https.onRequest(app);
I guess my problem is how to bridge the two functions.
What you're trying to do isn't possible. You can't dynamically create an API endpoint using data that created in another trigger. Everything for the express app needs to be defined at the top level of your source code, including all the URL paths, so it can be passed fully to functions.https.onRequest. Other triggers cannot influence that endpoint at all - they each run fully independently of each other.
If you want to change the behavior of your endpoint using data from Firestore, you will have to query Firestore directly from the endpoint code.

Can't return value inside Async Function

I have a file dedicated to handling Authentication functions for my React Native App. One of these functions getJWToken is an anonymous async function that tries to get the current user token by the following means:
const auth = db.auth()
// db === (Firebase.apps.length === 0 ? Firebase.initializeApp(config) : Firebase.app())
// the configuration of the Firebase DB occurs in a config file and db is exported.
const getJWToken = async () => {
auth.onAuthStateChanged(function(user) {
if (user) {
user.getIdToken()
.then(function(idToken) {
return idToken;
})
.catch(
(error) => {
console.log("Cannot get User Token: ", error);
}
)
}
});
}
Through console.log I'm able to see that the function returns the token as it's suppose to. The issues arrives when I'm using it in another async function in another file like so:
Service Folder index.js
import * as Auth from './AuthServices'
export {Auth}
App.tsx
// App.tsx
import {Auth} from './src/services'
// The rest in inside an Async Function
signOut: async () => {
let token
token = await Auth.getJWToken()
console.log("token: ", token) // results in undefined
}
Token results in undefined. I have no means of returning the token value from the promise function. I've tried using return on auth.onAuthStateChanged but that results in token evaluating to [Function Bound]. await on auth.onAuthStateChanged does nothing either. I'm truly stumped.
You have to return at two more places as show in the code . Please reply with this modification, ket us c
const auth = db.auth()
// db === (Firebase.apps.length === 0 ? Firebase.initializeApp(config) : Firebase.app())
// the configuration of the Firebase DB occurs in a config file and db is exported.
const getJWToken = async () => {
return auth.onAuthStateChanged(function(user) {
if (user) {
return user.getIdToken()
.then(function(idToken) {
return idToken;
})
.catch(
(error) => {
console.log("Cannot get User Token: ", error);
}
)
}
});
}
While I no longer use this code snippet I was able to solve it like so:
getJWToken
export const getJWToken = async () => {
let result;
// await is needed here
await auth.onAuthStateChanged(function(user) {
if (user) {
console.log("user")
// save this promise to result
result = user.getIdToken()
.then(function(idToken) {
return idToken;
})
.catch(
(error) => {
console.log("Cannot get User Token: ", error);
}
)
}
});
// return the user.getToken() promise
return result

Unable to return data from cloud function using https callable

I am calling the following cloud function from my Angular application, but the value returned is always null even though the cloud function is correctly logging the result. Not sure what I am doing incorrectly.
My Angular code is as follows:
const data = {test: testToProcess};
const process = this.fns.httpsCallable("gprocess"); // fns is Angular Fire Functions
process(data)
.toPromise() // since Angular Fire function returns an observable, Iam converting to a promise
.then(
(result) => {
console.log("function called: " + JSON.stringify(result));
},
(err) => {
console.log("Function call error " + JSON.stringify(err));
}
);
My cloud functions code is as follows:
import * as functions from "firebase-functions";
const fs = require("fs");
const { google } = require("googleapis");
const script = google.script("v1");
const scriptId = "MY_SCRIPT_ID";
export const gprocess = functions.https.onCall(async (data: any, context: any) => {
const test = data.test;
return fs.readFile("gapi_credentials.json", (err: any, content: string) => {
if (err) {return err;}
const credentials = JSON.parse(content); // load the credentials
const { client_secret, client_id, redirect_uris } = credentials.web;
const googleAuth = require("google-auth-library");
const functionsOauth2Client = new googleAuth.OAuth2Client(client_id, client_secret, redirect_uris); // Constuct an auth client
functionsOauth2Client.setCredentials({refresh_token: credentials.refresh_token,}); // Authorize a client with credentials
return runScript(functionsOauth2Client,scriptId,JSON.stringify(test))
.then((scriptData: any) => {
console.log("Script data in main function is" + JSON.stringify(scriptData));
return scriptData;
})
.catch((error) => {return error;});
});
});
function runScript(auth: any, scriptid: string, test: any) {
return new Promise(function (resolve, reject) {
script.scripts.run({
auth: auth,
scriptId: scriptid,
resource: {function: "doGet",devMode: true,parameters: test}
})
.then((err: any, respons: any) => {
if (err) {
console.log("API returned an error: " + JSON.stringify(err));
resolve(err);
} else if (respons) {
console.log("Script is run and response is " + JSON.stringify(respons));
resolve(respons);
}
});
});
}
The angular function is returning this result before the processing on the cloud function can be completed. IT is not waiting for the results to be returned from the cloud function.
detailed.component.ts:691 function called: null
After some time the results are logged on the cloud function console but this is not returned back to the angular client. The log on the cloud function is as follows and as shown below the correct result is logged:
5:53:32.633 PM gpublish Function execution started
5:53:32.694 PM gpublish Function execution took 61 ms, finished with status code: 204
5:53:33.185 PM gpublish Function execution started
5:53:33.804 PM gpublish Function execution took 620 ms, finished with status code: 200
5:54:31.494 PM gpublish Script is run and response is : {"config":... some result}
5:54:31.593 PM gpublish Script data in main function is{"config":... some result}
Please help!
Your function is not correctly returning a promise that resolves to the data to serialize and send to the client. The problem is that fs.readFile doesn't actually return a promise. It's asychronous and returns nothing, and that's what the client will receive. The callback is being invoked, but nothing inside that callback will affect what the client sees.
You will need to find another way of doing your file I/O that is either synchronous (such as fs.readFileSync), or actually works with promises instead of just an async callback.
I changed my code to use readFileSync as mentioned above (Thank you Doug!)
In addition, I stringified the data being sent back from the cloud function to the Angular client.
My cloud function now looks like this:
import * as functions from "firebase-functions";
const fs = require("fs");
const { google } = require("googleapis");
const script = google.script("v1");
const scriptId = "MY_SCRIPT_ID";
export const gprocess = functions.https.onCall(
async (data: any, context: any) => {
try {
const test = data.test;
const content = fs.readFileSync("credentials.json"); // CHANGED TO READFILESYNC
const credentials = JSON.parse(content);
const { client_secret, client_id, redirect_uris } = credentials.web;
const googleAuth = require("google-auth-library");
const functionsOauth2Client = ... some code to construct an auth client and authorise it
return runScript(functionsOauth2Client,scriptId,JSON.stringify(test)).then((scriptData: any) => {
return JSON.stringify(scriptData); // STRINGIFIED THE DATA
});
} catch (err) {
return JSON.stringify(err);
}
});
function runScript(auth: any, scriptid: string, test: any) {
return new Promise(function (resolve, reject) {
script.scripts.run({auth: auth,scriptId: scriptid,resource: {function: "doGet",parameters: test}})
.then((respons: any) => {resolve(respons.data);})
.catch((error: any) => {reject(error);});
});
}

Unit Test: Stub/rewire a function inside a server request

I want to test a route that makes external api calls.
I would like to stub the functionThatShouldBeStubbed so I can skip the external api call and focus on testing the route instead.
I am using Sinon and rewire, because if I understood correctly I cannot stub a function that was exported the way it currently is.
However, it seems like even though rewire replaced the function, my test is still making external api call. It seems like sinon is not aware that the function was rewired. How can I make this situation work?
//--------------------------
//../target.js
const functionThatShouldBeStubbed = async () => {
const results = await external_API_call();
return results;
}
module.exports = {
functionThatShouldBeStubbed,
/*more other functions*/
}
//--------------------------
//../index.js
app.use(require('endpoint.js'));
//--------------------------
//endpoint.js
const { functionThatShouldBeStubbed } = require("target.js");
router.post('endpoint', async(req, res) => {
//do lots of stuff
const results = await functionThatShouldBeStubbed();
if(results.error) { return res.status(207).send({ /*stuff */})}
//...more stuff
})
//--------------------------
//test.js
const server = require("../index.js");
const rewire = require('rewire')
const restoreTarget = rewire('../target.js');
describe("Should return appropriate error code to requester", function () {
it("Should return 207 in this case", function (done) {
const targetStub = sinon.stub().resolves({msg: 'fake results', statusCode: 207})
const targetRewired = restoreTarget.__set__("functionThatShouldBeStubbed", targetStub);
chai.request(server)
.post("/endpoint")
.send('stuff over')
.catch((error) => {
console.log("Error: ", error)
done();
})
.then((res) => {
expect(targetStub.callCount).to.equal(1);
res.should.have.status(207);
restoreTarget();
targetStub.restore();
done();
})
})
})
Many thanks!
Edit: updated code for more detail
Edit2: updated code again to show import method
You shouldn't need rewire at all here based on how your module is being exported. The following should work
//test.js
const target = require ("../target");
const server = require("../index");
describe("Should return appropriate error code to requester", () => {
it("Should return 207 in this case", done => {
const targetStub = sinon
.stub(target, "functionThatShouldBeStubbed")
.resolves({msg: 'fake results', statusCode: 207})
chai.request(server)
.post("/endpoint")
.send('stuff over')
.then(res => {
expect(targetStub.callCount).to.equal(1);
res.should.have.status(207);
targetStub.restore();
done();
})
})
})

Jest testing mongoose model instantiation

I'm trying to test a REST API built with express and mongoose, I'm using jest and supertest for the http calls; also I'm relatively new to testing with javascript.
When testing a creation url I wan't to make sure the instantiation is called using just the req.body object but I'm not sure how to do it, after reading a lot about differences between mock objects and stubs and some of the Jest documentation my last try looks like this:
test('Should instantiate the model using req.body', done => {
const postMock = jest.fn();
const testPost = {
name: 'Test post',
content: 'Hello'
};
postMock.bind(Post); // <- Post is my model
// I mock the save function so it doesn't use the db at all
Post.prototype.save = jest.fn(cb => cb(null, testPost));
// Supertest call
request(app).post('/posts/')
.send(testPost)
.then(() => {
expect(postMock.mock.calls[0][0]).toEqual(testPost);
done();
})
.catch(err => {throw err});
});
Also I would like to know how to manually fail the test on the promise rejection, so it doesn't throws the Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
As it stands, you're performing more of a integration test rather than isolating the route handler function itself and testing just that.
First I would break away the handler for /posts/ to its own file (assuming you haven't done this already):
controllers/post-controller.js
const Post = require('./path/to/models/post')
exports.store = async (req, res) => {
const post = await new Post(req.body).save()
res.json({ data: post }
}
Next simply use the handler wherever you defined your routes:
const express = require('express')
const app = express()
const postController = require('./path/to/controllers/post-controller')
app.post('/posts', postController.store)
With this abstraction we can now isolate our postController.store and test that it works with req.body. Now since we need to mock mongoose to avoid hitting an actual database, you can create a mocked Post like so (using the code you already have):
path/to/models/__mocks__/post.js
const post = require('../post')
const mockedPost = jest.fn()
mockedPost.bind(Post)
const testPost = {
name: 'Test post',
content: 'Hello'
}
Post.prototype.save = jest.fn(cb => {
if (typeof cb === 'function') {
if (process.env.FORCE_FAIL === 'true') {
process.nextTick(cb(new Error(), null))
} else {
process.nextTick(cb(null, testPost))
}
} else {
return new Promise((resolve, reject) => {
if (process.env.FORCE_FAIL === 'true') {
reject(new Error())
} else {
resolve(testPost)
}
})
}
})
module.exports = mockedPost
Notice the check for process.env.FORCE_FAIL if for whatever reason you wanted to fail it.
Now we're ready to test that using the req.body works:
post-controller.test.js
// Loads anything contained in `models/__mocks__` folder
jest.mock('../location/to/models')
const postController = require('../location/to/controllers/post-controller')
describe('controllers.Post', () => {
/**
* Mocked Express Request object.
*/
let req
/**
* Mocked Express Response object.
*/
let res
beforeEach(() => {
req = {
body: {}
}
res = {
data: null,
json(payload) {
this.data = JSON.stringify(payload)
}
}
})
describe('.store()', () => {
test('should create a new post', async () => {
req.body = { ... }
await postController(req, res)
expect(res.data).toBeDefined()
...
})
test('fails creating a post', () => {
process.env.FORCE_FAIL = true
req.body = { ... }
try {
await postController.store(req, res)
} catch (error) {
expect(res.data).not.toBeDefined()
...
}
})
})
})
This code is untested, but I hope it helps in your testing.

Categories

Resources