How to send context parameter in unit test to a firebase functions running on the functions emulator - google-cloud-functions

I manage to start the firebase emulators and load a cloud function. Now I want to write a test.
PROBLEM I use chai-http to call the function in the emulator but I do not succeed in sending a context to the function.
when calling the function using chai-http, I see the following warning and error :
{"severity":"WARNING","message":"Request body has extra fields: context"}
{"severity":"ERROR","message":"Invalid request, unable to process."}
Here is the test code snippet :
it("function_call", async() => {
await new Promise((resolve, reject) => {
chai.request(url)
.post("")
.send({
data: {
id: FILE_ID
},
context: {
auth: {
uid: USER_ID,
token: USER_TOKEN
}
}
})
.end(function(err, res) {
console.info(JSON.stringify(res));
const payload = JSON.parse(res.text);
chai.expect(payload.error).not.null;
resolve();
});
});
// expect some data from firestore emulator to be deleted
const afterCAll = await firestore.collection(`users/${USER_ID}/files/${FILE_ID}`).get();
chai.expect(afterCAll.empty).is.true;
});
And here is the function code :
export const doSomething = async(data, context) => {
console.log("context=" + JSON.stringify(context));
console.log("context.auth=" + JSON.stringify(context.auth));
}
export const CLOUD_FUNCTION = functions
.runWith(runtimeOpts)
.region("REGION")
.https
.onCall(doSomething);
And to execute the test, i run :
firebase emulators:exec --project dev-export 'npm test --prefix functions --verbose --debug'
Leaving out the context in the parameter :
chai.request(url)
.post("")
.send({
data: {
id: FILE_ID
}
})
and the function call to the emulator works just fine

Looking for similar cases with the error you provided, I have a found a question that tries to get user credentials from context, and I noticed the functions they were using were not asynchronous, you might want to check context, and authData .
You can try:
exports.doSomething = functions.https.onCall((data, context) => {
const result = context;
const result= context.auth;
console.log("context=" + JSON.stringify(result));
console.log("context.auth=" + JSON.stringify(resultAuth));
}

Related

Firebase Cloud Function - RangeError: Maximum call stack size exceeded

I am trying to write an onCall Firebase Cloud Function that calls an external Zoho Desk API to return a list of support tickets.
But whenever I call my Firebase Cloud Function it returns this error: Unhandled error RangeError: Maximum call stack size exceeded.
Most of the other answers I have found are in relation to Firebase document snapshots, and they say it is caused by a infinite loop. But I'm not sure how to apply that knowledge to my external api call.
Here is the Cloud Function in question:
export const getZohoDeskTickets = functions
.region('us-central1')
.https.onCall(async (data, context) => {
// Check if it passed App Check
if (context.app == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.'
);
}
// Check the authentication
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError(
'unauthenticated',
'The function must be called while authenticated.'
);
}
// Do the operation
const zohoAccessToken = await getSecretVersion(
'zoho-self-client-api-access-token'
).catch((error) => {
throw new functions.https.HttpsError('unknown', error.message, error);
});
return axios
.get('https://desk.zoho.com/api/v1/tickets', {
headers: {
orgId: '123456789',
Authorization: `Zoho-oauthtoken ${zohoAccessToken}`,
},
})
.catch(async (error) => {
functions.logger.error(error.response.data);
throw new functions.https.HttpsError(
'unknown',
error.response.data.message,
error.response.data
);
});
});
UPDATE:
I found this helpful thread which showed that sometimes the infinite loop is not coming from the Cloud Function but the caller, and also that the call stack can be helpful for debugging.
In the interest of that, here is my call stack as shown in the Firebase Emulator logs. It doesn't make sense to me because it's just the same line repeated again and again:
Unhandled error RangeError: Maximum call stack size exceeded
at TCP.get [as reading] (_tls_wrap.js:617:7)
at Function.entries (<anonymous>)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:157:37)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:158:22)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:158:22)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:158:22)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:158:22)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:158:22)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:158:22)
at encode (/Users/macbook/Dev/my-app/functions/node_modules/firebase-functions/lib/common/providers/https.js:158:22)
And here is the Chrome browser console call stack:
postJSON # index.esm2017.js?6f1f:499
call # index.esm2017.js?6f1f:553
await in call (async)
eval # index.esm2017.js?6f1f:485
eval # SupportPage.vue?c3cc:37
eval # index.js??clonedRule…tup=true&lang=ts:15
__awaiter # index.js??clonedRule…tup=true&lang=ts:11
handleClick # SupportPage.vue?c3cc:36
callWithErrorHandling # runtime-core.esm-bundler.js?f781:155
callWithAsyncErrorHandling # runtime-core.esm-bundler.js?f781:164
emit$1 # runtime-core.esm-bundler.js?f781:718
eval # runtime-core.esm-bundler.js?f781:7232
onClick # QBtn.js?9c40:148
callWithErrorHandling # runtime-core.esm-bundler.js?f781:155
callWithAsyncErrorHandling # runtime-core.esm-bundler.js?f781:164
invoker # runtime-dom.esm-bundler.js?9cec:366
And here is the calling function inside my Vue app:
<template>
<v-btn design="alpha" #click="handleClick">loadData</v-btn>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import VBtn from 'src/components/VBtn.vue';
import { httpsCallable, FunctionsError } from 'firebase/functions';
import { functions } from 'src/config/firebase';
const data = ref();
const getZohoDeskTickets = httpsCallable(functions, 'getZohoDeskTickets');
const isFunctionsError = (error: unknown): error is FunctionsError => {
return (error as FunctionsError).details !== undefined;
};
const handleClick = async () => {
const ticket = await getZohoDeskTickets().catch((error) => {
if (isFunctionsError(error)) {
console.log(error.code);
console.log(error.message);
console.log(error.details);
} else {
console.log(error);
}
});
data.value = ticket;
console.log(ticket);
return ticket;
};
</script>
But even with that I still cannot figure this out.
What is causing the infinite loop?
Or maybe it is something else causing this error?
Finally got it!
The solution came from this answer.
In short; I needed to add a .then() onto the returned axios chain like so:
export const getZohoDeskTickets = functions
.region('us-central1')
.https.onCall(async (data, context) => {
// Check if it passed App Check
if (context.app == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.'
);
}
// Check the authentication
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError(
'unauthenticated',
'The function must be called while authenticated.'
);
}
// Do the operation
const zohoAccessToken = await getSecretVersion(
'zoho-self-client-api-access-token'
).catch((error) => {
throw new functions.https.HttpsError('unknown', error.message, error);
});
return axios
.get('https://desk.zoho.com/api/v1/tickets', {
headers: {
orgId: '774638961',
Authorization: `Zoho-oauthtoken ${zohoAccessToken}`,
},
})
.then((response) => {
functions.logger.info(response.data);
return response.data;
})
.catch((error) => {
functions.logger.error(error.response.data);
throw new functions.https.HttpsError(
'unknown',
error.response.data.message,
error.response.data
);
});
});

Firebase Emulator Suite - simple pubsub example

I have read MANY docs/blogs/SO articles on using the Firebase Emulator Suite trying a simple pubsub setup, but can't seem to get the Emulator to receive messages.
I have 2 functions in my functions/index.js:
const functions = require('firebase-functions');
const PROJECT_ID = 'my-example-pubsub-project';
const TOPIC_NAME = 'MY_TEST_TOPIC';
// receive messages to topic
export default functions.pubsub
.topic(TOPIC_NAME)
.onPublish((message, context) => {
console.log(`got new message!!! ${JSON.stringify(message, null, 2)}`);
return true;
});
// publish message to topic
export default functions.https.onRequest(async (req, res) => {
const { v1 } = require('#google-cloud/pubsub');
const publisherClient = new v1.PublisherClient({
projectId: process.env.GCLOUD_PROJECT,
});
const formattedTopic = publisherClient.projectTopicPath(PROJECT_ID, TOPIC_NAME);
const data = JSON.stringify({ hello: 'world!' });
// Publishes the message as JSON object
const dataBuffer = Buffer.from(data);
const messagesElement = {
data: dataBuffer,
};
const messages = [messagesElement];
// Build the request
const request = {
topic: formattedTopic,
messages: messages,
};
return publisherClient
.publish(request)
.then(([responses]) => {
console.log(`published(${responses.messageIds}) `);
res.send(200);
})
.catch((ex) => {
console.error(`ERROR: ${ex.message}`);
res.send(555);
throw ex; // be sure to fail the function
});
});
When I run firebase emulators:start --only functions,firestore,pubsub and then run the HTTP method with wget -Sv -Ooutput.txt --method=GET http://localhost:5001/my-example-pubsub-project/us-central1/httpTestPublish, the HTTP function runs and I see its console output, but I can't seem to ever get the .onPublish() to run.
I notice that if I mess around with the values for v1.PublisherClient({projectId: PROJECT_ID}), then I will get a message showing up in the GCP cloud instance of the Subscription...but that's exactly what I don't want happening :)

Stripe Error Message 405 - "append .json to uri to use rest api"

I'm using Stripe, and trying to send a test webhook to my URL and database hosted by Firebase. When I "send test webhook," I get the following error message in the Stripe Console:
Test Webhook Error: 405
"append .json to your request URI to use the rest API"
My code is a direct copy of the tutorial: https://github.com/GaryH21/Stripe-Webhooks-Tutorial/blob/master/functions/index.js
Here is the code of my index.js:
const functions = require('firebase-functions');
const stripe = require("stripe")(functions.config().keys.webhooks);
const admin = require('firebase-admin')
admin.initializeApp();
const endpointSecret = functions.config().keys.signing;
exports.events = functions.https.onRequest((request, response) => {
let sig = request.headers["stripe-signature"];
try {
let event = stripe.webhooks.constructEvent(request.rawBody, sig, endpointSecret)
return admin.database().ref('/events').push(event)
.then((snapshot) => {
return response.json({ received: true, ref: snapshot.ref.toString() })
})
.catch((err) => {
console.error(err)
return response.status(500).end() // error saving to database
})
} catch (err) {
return response.status(400).end() // signing signature failed
}
})
exports.exampleDataBaseTrigger = functions.database.ref('/events/{eventId}').onCreate((snapshot, context) => {
return console.log({
eventId: context.params.eventid,
data: snapshot.val()
})
})
The only time in the tutorial and in my code that .json is used is in the line: return response.json({ received: true, ref: snapshot.ref.toString() })
Should I be appending .json onto "request" somewhere, such as in request.RawBody?
It isn't a problem with the signing keys, as that would give the 400 Error message, which I already dealt with and fixed.
I would be happy to share the code of other files in my app, but as far as I can tell none of the rest is relevant to the problem. Thank you very much.

GraphQL: fulfill query from JSON file source

I've just started messing about with GraphQL, and I'd like a resolver that uses a JSON file on disk as the data source. What I've got so far causes GraphQL to return null.
How do I do this and why doesn't the approach below work?
var schema = buildSchema(`
type Experiment {
id: String
trainData: String
goldData: String
gitCommit: String
employee: String
datetime: String
}
type Query {
# Metadata for an individual experiment
experiment: Experiment
}
schema {
query: Query
}`);
var root = {
experiment: () => {
fs.readFile('./data/experimentExample.json', 'utf8', function(err, data) {
if (err) throw err;
console.log(data);
return JSON.parse(data);
});
}
};
const app = express();
app.use('/graphql', graphqlHTTP({
rootValue: root,
schema: schema,
graphiql: true
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');
The callback function you're passing to readFile runs asynchronously, which means returning a value from it doesn't do anything -- the function the readFile call is inside is done executing and has returned a value (null) by the time your callback is done.
As a rule of thumb, when dealing with GraphQL, you should stay away from callbacks -- your resolvers should always return a value or a Promise that will eventually resolve to a value.
Luckily, fs has an asynchronous method for reading files, so you can just do:
const root = {
experiment: () => {
const file = fs.readFileSync('./data/experimentExample.json', 'utf8')
return JSON.parse(file)
}
};
// or even cleaner:
const root = {
experiment: () => JSON.parse(fs.readFileSync('./data/experimentExample.json', 'utf8'))
};
As an additional example, here's how you would do that with a Promise:
// using Node 8's new promisify for our example
const readFileAsync = require('util').promisify(fs.readFile)
const root = {
experiment: () => readFileAsync('./data/experimentExample.json', {encoding: 'utf8'})
.then(data => JSON.parse(data))
};
// Or with async/await:
const root = {
experiment: async () => JSON.parse(await readFileAsync('./data/experimentExample.json', {encoding: 'utf8'}))
};
Of course there's no need to promisify readFile since you already have an async method available, but this gives you an idea of how to work with Promises, which GraphQL is happy to work with.

Testing a MySQL INSERT result with Node.js/Sinon

My application code to test:
create(params) {
let result = {};
try {
result = await this.db.query('/* some query */');
} catch (e) {
throw new Error('Error creating User', e);
}
return this._getById(result.insertId);
}
I have the _getById method in the same class which does exactly what it says...
And my current test (running through Ava):
test('it should create a user', async t => {
const db = mock({
query: () => {
},
});
const userObj = {
// some params
};
const user = new Users({
db: db.object,
});
const call = {
request: userObj,
};
const result = await user.create(call);
// test?
});
If I try and test anything based off of the result variable, ie. the newly created User, I receive the error "Cannot read property 'insertId' of undefined". What is my best option with Sinon to test that this create method will return a newly created "user"?
I think that you have the "Cannot read property 'insertId' of undefined" because the following mock doesn't return something
const db = mock({
query: () => {
},
});
If you return something like that
const db = mock({
query: () => {
return {
username: "username"
}
},
});
and in the test the result will have the value and you should be able to use expect to check if you have the expected result:
{
username: "username"
}
The problem in this specific test starts when the mock does not return something and also calling mock overrode the value that you assigned here
let result = {};