Flakey tests when testing firebase functions using Jest - node.js

I'm testing Firebase functions using Jest and the emulator, though the tests are flakey presumably from a race condition. By flakey, I mean sometimes they pass and sometimes they don't, even on the same machine.
Tests and functions are written in TypeScript, then transpiled with babel.
Example test/function
Note: This is an example of just one of the flakey tests. Many other tests are flakey. A solution is preferably one that doesn't just solve this one case, but rather the general issue.
The test
import { onProfilesWrite } from '../src/profiles/on-write'
import { initializeAdminApp } from '#firebase/rules-unit-testing'
const admin = initializeAdminApp({ projectId: 'projectId' }).firestore()
const wrappedFunction = testEnvironment.wrap(onProfilesWrite)
const profilePath = `profiles/${uid}`
const customerProfile = {
roles: ['customer'],
slug: 'slug',
image: 'image.png',
fullName: 'John Smith',
}
const publisherRoles = ['customer', 'publisher']
const publisherProfile = {
...customerProfile,
roles: publisherRoles,
}
const createChange = async (
before: Record<string, unknown> | undefined,
changes: Record<string, unknown>
) => {
const publisherStatsRef = admin.doc(profilePath)
if (before) await publisherStatsRef.set(before)
const beforeSnapshot = await publisherStatsRef.get()
await publisherStatsRef.set(changes, { merge: true })
const afterSnapshot = await publisherStatsRef.get()
return testEnvironment.makeChange(beforeSnapshot, afterSnapshot)
}
test('If user profile is created as a publisher, publisherDetails is created', async () => {
const change = await createChange(undefined, publisherProfile)
await wrappedFunction(change)
const snapshot = await admin.doc(`profileDetails/${uid}`).get()
const data = snapshot.data()
expect(data).toBeTruthy()
expect(data?.id).toBeTruthy()
expect(data?.slug).toBe(publisherProfile.slug)
expect(data?.profileImage).toBe(publisherProfile.image)
expect(data?.publisherName).toBe(publisherProfile.fullName)
expect(data?.music).toMatchObject([])
})
Run the test
firebase emulators:exec \"jest functions/__tests__ --detectOpenHandles\" --only firestore
Output
If user profile is created as a publisher, publisherDetails is created
expect(received).toBeTruthy()
Received: undefined
46 | const snapshot = await admin.doc(`profileDetails/${uid}`).get()
47 | const data = snapshot.data()
> 48 | expect(data).toBeTruthy()
| ^
49 | expect(data?.id).toBeTruthy()
50 | expect(data?.slug).toBe(publisherProfile.slug)
51 | expect(data?.profileImage).toBe(publisherProfile.image)
The function
import * as functions from 'firebase-functions'
// initializes the admin app, then exports admin.firestore
import { firestore } from '../admin'
const database = firestore()
const createPublisherId = async (): Promise<string> => {
let id = ''
const MAX_NUMBER = 1000000
while (id === '') {
const temporaryId = String(Math.ceil(Math.random() * MAX_NUMBER))
const snapshot = await firestore()
.collection('publisherDetails')
.where('sku', '==', temporaryId)
.limit(1)
.get()
if (snapshot.empty) id = temporaryId
}
return id
}
export const createPublisherDetails = async (
newData: firestore.DocumentData,
uid: string
): Promise<void> => {
const id = await createPublisherId()
await database.doc(`publisherDetails/${uid}`).set(
{
id,
slug: newData.slug,
publisherName: newData.fullName,
profileImage: newData.image,
music: [],
},
{ merge: true }
)
}
export const onProfilesWrite = functions.firestore.document('profiles/{uid}').onWrite(
async (change): Promise<void> => {
const { id: uid } = change.after
const oldData = change.before.data()
const newData = change.after.data()
if (
newData?.roles?.includes('publisher') &&
(typeof oldData === 'undefined' || !oldData.roles?.includes('publisher'))
)
await createPublisherDetails(newData, uid)
}
)
Debug steps
All promises are awaited in the cloud functions (as affirmed by an ESLint rule #typescript-eslint/no-floating-promises)
Also converted the tests to Mocha (as suggested by the Firebase docs), same errors
Converting async/await in tests to promise.then() syntax
Metadata
OS: macOS 11.2, Ubuntu 18.04
Jest: 26.6.3
Firebase: 8.2.6
Firebase tools: 9.3.0
As comments roll in, with either questions or suggestions, I'll continue to update this post.

Change your test portion to as follows :
test('If user profile is created as a publisher, publisherDetails is created', async () => {
const change = await createChange(undefined, publisherProfile)
await wrappedFunction(change)
const documentObject = await admin.doc(`profileDetails/${uid}`)
const snapshot = await documentObject.get()
const data = snapshot.data()
expect(data).toBeTruthy()
expect(data?.id).toBeTruthy()
expect(data?.slug).toBe(publisherProfile.slug)
expect(data?.profileImage).toBe(publisherProfile.image)
expect(data?.publisherName).toBe(publisherProfile.fullName)
expect(data?.music).toMatchObject([])
})
Reason being that in your test region, your use of await is a bit incorrect (function chaining on an object that is being waited for is a big no-no in the same calling line)

Related

Issue with update data by postgres pg package

I have an app built with angular and node.js (with pg npm package, version = 8.7.1)
The app divided to microservice . Each server-app have "pg" package installed and have a connection to postgres db.
The problem is that if I run some "update" query and after this I running getList query, then I Got the old value instead the updated object. If I add setTimeout for 5 sec then it works fine
On my localhost all works fine. The issue occur only on heroku (with postgres on cloud) on the srever. sometimes I got the updated data and sometimes not
Here is my code:
Client code (angular) - calling to update func and then getList func with async & await
async filter({ value }) {
const list: any = await this.getList()
const [myData]: any = await this.updateData(this.value)
const list: any = await this.getList() // Here is the issue !!
}
The function calls to API to the server like this:
getList(): Promise<any> {
return this.http.get<any>(`${ENV.BASE_API}/doGetApiCalls`).toPromise();
}
updateData(value: any): Promise<any> {
return this.http.put<any>(`${ENV.BASE_API}/doUpdateApiCalls`, value).toPromise();
}
The server code is:
Bl code
async function updateData(description, id) {
let query = updateDataQuery(description, id);
let results = await postgressQuery(query);
return getDataResults;
}
DEL code
function updateDataQuery(description: string, id:number) {
const query = `UPDATE public.books
SET description='${description}',
WHERE book =${id}
RETURNING *`
return query;
}
And here is the connection to postgres db (BL calling to lib by import this)
const DATABASE_URL = process.env.DATABASE_URL;
const pool = new Pool({
connectionString:DATABASE_URL,
ssl:{rejectUnauthorized: false}
})
let openConnect = async () => {
await pool.connect();
}
let postgressQuery = async (q) => {
try {
const result = await pool.query(q);
return await result.rows;
}
catch (e) {
console.log(e);
}
}
========================================================
If I added await to client then it works fine. It takes a while for update?
async filter({ value }) {
const list: any = await this.getList() //
const [myData]: any = await this.updateData(this.value) //get the RETURN from server with correct data
await new Promise(resolve => setTimeout(resolve, 5000)) //added for wait for 5 sec
const list: any = await this.getList() // then data is correct (aafer 5 sec)
}
What wrong in this code above ?
Thanks in advance
I found a solution:
It was cache issue. Just need to install this package and all fine !
https://www.npmjs.com/package/nocache

Unable to stub an exported function with Sinon

I need to test the following createFacebookAdVideoFromUrl() that consumes a retryAsyncCall that I'd like to stub with Sinon :
async function createFacebookAdVideoFromUrl(accountId, videoUrl, title, facebookToken = FACEBOOK_TOKEN, options = null, businessId = null) {
const method = 'POST';
const url = `${FACEBOOK_URL}${adsSdk.FacebookAdsApi.VERSION}/${accountId}/advideos`;
const formData = {
access_token: businessId ? getFacebookConfig(businessId).token : facebookToken,
title,
name: title,
file_url: videoUrl,
};
const callback = () => requestPromise({ method, url, formData });
const name = 'createFacebookAdVideoFromUrl';
const retryCallParameters = buildRetryCallParameters(name, options);
const adVideo = await retryAsyncCall(callback, retryCallParameters);
logger.info('ADVIDEO', adVideo);
return { id: JSON.parse(adVideo).id, title };
}
This retryAsyncCall function is exported as such:
module.exports.retryAsyncCall = async (callback, retryCallParameters, noRetryFor = [], customRetryCondition = null) => {
// Implementation details ...
}
Here is how I wrote my test so far:
it.only("should create the video calling business's Facebook ids", async () => {
const payload = createPayloadDataBuilder({
businessId: faker.internet.url(),
});
const retryAsyncCallStub = sinon.stub(retryAsyncCallModule, 'retryAsyncCall').resolves('random');
const createdFacebookAd = await FacebookGateway.createFacebookAdVideoFromUrl(
payload.accountId,
payload.videoUrl,
payload.title,
payload.facebookToken,
payload.options,
payload.businessId,
);
assert.strictEqual(retryAsyncCallStub.calledOnce, true);
assert.strictEqual(createdFacebookAd, { id: 'asdf', title: 'asdf' });
});
I don't expect it to work straightaway as I am working in TDD fashion, but I do expect the retryAsyncCall to be stubbed out. Yet, I am still having this TypeError: Cannot read property 'inc' of undefined error from mocha, which refers to an inner function of retryAsyncCall.
How can I make sinon stubbing work?
I fixed it by changing the way to import in my SUT :
// from
const { retryAsyncCall } = require('../../../helpers/retry-async');
// to
const retry = require('../../../helpers/retry-async');
and in my test file :
// from
import * as retryAsyncCallModule from '../../../src/common/helpers/retry-async';
// to
import retryAsyncCallModule from '../../../src/common/helpers/retry-async';
The use of destructuring seemed to make a copy instead of using the same reference, thus, the stub was not applied on the right reference.

What To Do Failed Compiling?

I am getting an error while running the command npm start. The file is created in PancakeSwap Frontend and I've been trying to fix this for a while, thanks for your help :)
Here is my App.js code:
import React, { useState, useEffect } from "react";
import SimpleStorageContract from "./contracts/SimpleStorage.json";
import getWeb3 from "./getWeb3";
import BlockchainContext from './BlockchainContext.js';
import "./App.css";
function App() {
const [storageValue, setStorageValue] = useState(undefined);
const [web3, setWeb3] = useState(undefined);
const [accounts, setAccounts] = useState([]);
const [contract, setContract] = useState([]);
useEffect(() => {
try {
// Get network provider and web3 instance.
const web3 = await getWeb3();
// // Use web3 to get the user's accounts.
const accounts = await web3.eth.getAccounts();
// // Get the contract instance.
const networkId = await web3.eth.net.getId();
const deployedNetwork = SimpleStorageContract.networks[networkId];
const contract = new web3.eth.Contract(
SimpleStorageContract.abi,
deployedNetwork && deployedNetwork.address,
);
// Set web3, accounts, and contract to the state, and then proceed with an // example of interacting with the contract's methods.
setWeb3(web3);
setAccounts(accounts);
setContract(contract);;
this.setState({ web3, accounts, contract: instance } catch (error) {
// Catch any errors for any of the above operations.
alert(
`Failed to load web3, accounts, or contract. Check console for details.`,
);
console.error(error);
const init = async() => {
}
init();
}, []);
useEffect(() => {
const load = async () => {
// Stores a given value, 5 by default.
await contract.methods.set(5).send({ from: accounts[0] });
// // Get the value from the contract to prove it worked.
const response = await contract.methods.get().call();
// // Update state with the result.
setStorageValue(response);
}
if(typeof web3 !== 'undefined'
&& typeof account !== 'undefined'
&& typeof contract !== 'undefined'{
load();
}
}, [web3, accounts, contract]);
if(typeof web3 === 'undefined') {
return <div>Loading Web3, account, and contract...</div>;
}
return (
<div className="App">
<BlockchainContext.Provider value={{web3, accounts, contract}}>
<h1>Good to Go!</h1>
<p>Your Truffle Box is installed and ready.</p>
<h2>Smart Contract Example</h2>
<p>
If your contracts compiled and migrated successfully, below will show
a stored value of 5 (by default).
</p>
<p>
Try changing the value stored on <strong>line 42</strong> of App.js.
</p>
<div>The stored value is: {storageValue}</div>
</BlockchainContext.Provider>
</div>
);
}
export default App;
And the error which I am getting is:
Failed to compile.
./src/App.js
Line 17:23: Parsing error: Can not use keyword 'await' outside an async function
15 | try {
16 | // Get network provider and web3 instance.
> 17 | const web3 = await getWeb3();
| ^
18 |
19 | // // Use web3 to get the user's accounts.
20 | const accounts = await web3.eth.getAccounts();
The code provided is pretty messy, but it looks like you are trying to use the await keyword in a synchronous function, specifically the function being passed as an argument into useEffect(). The await keyword can only be used inside asynchronous functions.
If you can, the easiest solution would be to make the function asynchronous by adding the async keyword (demonstrated below).
function App() {
const [storageValue, setStorageValue] = useState(undefined);
const [web3, setWeb3] = useState(undefined);
const [accounts, setAccounts] = useState([]);
const [contract, setContract] = useState([]);
useEffect(async () => {
If that won't work, you could use .then() instead (demonstrated below), however this will require more work.
function App() {
const [storageValue, setStorageValue] = useState(undefined);
const [web3, setWeb3] = useState(undefined);
const [accounts, setAccounts] = useState([]);
const [contract, setContract] = useState([]);
useEffect(() => {
try {
// Get network provider and web3 instance.
getWeb3().then(web3=>{
// code goes here
}).catch(err=>{
// error handling
});
I also recommend reading a little about async/await if you haven't already. This MDN article is a good place to start.

Discord.js/Firestore .where() is not a function

I am trying to integrate my discord bot with firestore. Whenever I try to run a query I get .where is not a function and I don't understand why because everything else seems to work. Here is the relevant code. I have tried the require of firebase at the top of Remove.js and that doesn't seem to do anything.
Here is my thought to how I believe it should be working right now.
I run node . and it then runs my index.js file.
On an interaction create (i.e. a slash command is created) it checks the command file and in this case it is the remove command
It calls execute(interaction, db) where interaction is the interaction slash command and db is the admin.Firestore() db reference from index.js. I am fully able to use get commands (i.e. that first chunk of code works before I try to delete)
Because this is a reference I should be able to call .where() based on the Firestore documentation and yet I am hit with the error "TypeError: db.collection(...).doc(...).collection(...).doc(...).where is not a function"
// Index.js
// General Setup
const { Client, Collection, Intents } = require('discord.js')
const config = require('./config.json')
const fs = require('fs')
// Bot Setup
const myIntents = new Intents();
myIntents.add(Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MEMBERS)
const bot = new Client({intents: myIntents});
// Firebase Setup
const firebase = require('firebase/app')
const fieldValue = require('firebase-admin').firestore.FieldValue
const admin = require('firebase-admin')
const serviceAccount = require('./serviceAccount.json')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
})
let db = admin.firestore();
// Command Setup
bot.commands = new Collection();
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'))
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
bot.commands.set(command.data.name, command);
}
// Bot Login
bot.once('ready', async () => {
console.log('Wheatley is online!');
});
bot.on('interactionCreate', async interaction => {
if (!interaction.isCommand()) {
return
}
const command = bot.commands.get(interaction.commandName)
if (!command) {
return
}
try {
await command.execute(interaction, db)
} catch (error) {
console.error(error)
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true})
}
});
bot.login(config.bot_token);
///Remove.js
const { SlashCommandBuilder } = require('#discordjs/builders');
require('#firebase/firestore');
module.exports = {
data: new SlashCommandBuilder()
.setName('remove')
.setDescription('Removes object from collection')
.addStringOption(option =>
option.setName('item')
.setDescription('Enter an item in the collection to remove')
.setRequired(true)
),
async execute(interaction, db) {
const itemName = await interaction.options.getString('item')
const itemToDelete = db.collection('items').doc(interaction.guildId).collection('items').doc(itemName);
const doc = await itemToDelete.get();
if(!doc.exists) {
return interaction.reply({
content: `${itemName} does not exist in the collection. Try using /list to check for the right name.`,
ephemeral: true
})
}
const ownerId = interaction.user.id
const snapshot = db.collection('items').doc(interaction.guildId).collection('items').doc(itemName).where("ownerId", "==", ownerId).get();
if(!snapshot.exists) {
return interaction.reply({
content: `You are not the owner of ${itemName}. Please contact owner to delete this from the collection`,
ephemeral: true
})
}
itemToDelete.delete();
return await interaction.reply(`${itemName} was removed from the collection!`)
},
};
You are using where on a document, as where is a query function that is only available to collections.
Just be warned that the snapshot will return an array of snapshots as it is a query, not a single document.
Try this instead:
const snapshot = db.collection('items').doc(interaction.guildId).collection('items').where("ownerId", "==", ownerId).get();

ReferenceError: FirebaseFirestore is not defined firebase functions

I'm trying to create a firebase function that triggers when object metadata on a cloud storage bucket changes, but when the function triggers I am getting an error:
ReferenceError: FirebaseFirestore is not defined
at addKapsulFromStorageObject
Here's the code:
import * as functions from "firebase-functions";
const { v4: uuidv4 } = require('uuid');
import admin = require('firebase-admin');
import { ObjectMetadata } from "firebase-functions/lib/providers/storage";
admin.initializeApp();
const database = admin.firestore();
database.settings({ ignoreUndefinedProperties: true })
const KAPSUL_COLLECTION_ID = 'kapsuls';
exports.onKapsulFileAdded = functions.storage.object().onFinalize(async (object) =>{
addKapsulFromStorageObject(object);
})
exports.onKapsulMetaDataUpdated = functions.storage.object().onMetadataUpdate(async (object) => {
addKapsulFromStorageObject(object);
});
async function addKapsulFromStorageObject(object: ObjectMetadata) {
const metaData = object.metadata;
if(metaData == null || metaData == undefined) return;
const kapsulId = uuidv4()
console.log("Adding Kapsul to firestore.")
await database.collection(KAPSUL_COLLECTION_ID).doc(kapsulId).set({
id: kapsulId,
coordinates: new FirebaseFirestore.GeoPoint(parseFloat( metaData['lat']), parseFloat(metaData['long'])),
first_name: metaData['first_name'],
last_name: metaData['last_name'],
date: FirebaseFirestore.Timestamp.fromDate(new Date(metaData['date'])),
is_online: JSON.parse(metaData['is_online']),
is_paid: JSON.parse(metaData['is_paid']),
always_unlockable: metaData['always_unlockable'],
title: metaData['title'],
unlock_distance: parseFloat(metaData['unlock_distance']),
video_url: object.mediaLink,
metaData: metaData['thumbnail_url']
});
}
I'm not really sure what's going on.
FirebaseFirestore is not defined but you are using it at 2 places:
date: FirebaseFirestore.Timestamp.fromDate(new Date(metaData['date'])),
// and
coordinates: new FirebaseFirestore.GeoPoint(parseFloat( metaData['lat']), parseFloat(metaData['long'])),
Try using admin.firestore instead:
coordinates: new admin.firestore.GeoPoint(parseFloat( metaData['lat']), parseFloat(metaData['long'])),
^^^^^^^^^^^^^^^
date: admin.firestore.Timestamp.fromDate(new Date(metaData['date'])),
If you were following any tutorial, then they may have declared that somewhere above like:
const FirebaseFirestore = admin.firestore

Resources