Jest doesn't catch error, when thrown from async/await function - node.js

Im testing some functions with MongoDB and Mongoose. For the tests i'm using Jest and mongo-memory-server. The function createUser(arg1, arg2) should throw a TypeError if there already exists a user with that discord id in the database.
createUser(arg1, arg2)
async function createUser(disc_id, moodleToken) {
const profileData = await fetchUser(disc_id);
if (profileData) {
throw TypeError("The account already exist in the database!");
}
const newUser = new profileModel({
discord_id: disc_id,
moodle_token: moodleToken,
})
await newUser.save();
return {
userId: newUser._id
};
}
fetchUser(disc_id) returns whether a user with the same discord id is found or not.
When testing with Jest i created the following test which passes just fine:
it("Should create a new user", async () => {
const discord_id = "3452357445";
const moodle_token = "34ffDSE8439Ujfe8f3jj";
const { userId } = await createUser(discord_id, moodle_token);
const user = await profileSchema.findById(userId);
expect(user.discord_id).toEqual(discord_id);
expect(user.moodle_token).toEqual(moodle_token);
})
Now i want to test if the TypeError is thrown when trying to create a new user, with the same discord id, i tried the following without any success:
describe("Error when", () => {
it("an existing matches discord id", async () => {
const discord_id = "3452357445";
const moodle_token = "34ffDSE8439Ujfe8f3jj";
await createUser(discord_id, moodle_token)
await expect(async () => {
createUser(discord_id, moodle_token);
}).rejects.toThrow(TypeError("The account already exist in the database!"))
})
})
When running the tests this is the output from the console:
FAIL test/manageUserDB.test.js
Create new user
✓ Should create a new user (66 ms)
Error when
✕ an existing matches discord id (3 ms)
● Create new user › Error when › an existing matches discord id
TypeError: The account already exist in the database!
11 |
12 | if (profileData) {
> 13 | throw TypeError("The account already exist in the database!");
| ^
14 | }
15 | const newUser = new profileModel({
16 | discord_id: disc_id,
at createUser (BoodleDB/manageUserDB.js:13:15)
at Object.<anonymous> (test/manageUserDB.test.js:29:13)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 0.6 s, estimated 1 s
Ran all test suites matching /.\/test/i.
EDIT
function fetchUser (disc_id) {
return profileModel.findOne({ discord_id: disc_id });
}

You need to await when you call createUser.
await expect(async () => {
await createUser(discord_id, moodle_token);
}).rejects.toThrow(TypeError("The account already exist in the database!"))

Related

Jest API testing giving false passes

I have a simple test script for my API, the specific route is not implemented at the moment. But for some reason tests pass. Here is the script:
const request = require('supertest')
const api = require('../api-server')
// testing data:
const pmOne = {
name: "Some Name",
tel: "234 123"
}
const pmTwo = {
name: "Some Other Name",
tel: "256 789"
}
describe('Basic CRUD API', () =>{
it('GET /pm --> array of all projectmanagers', () =>{
request(api)
.post('/pm')
.send(pmTwo)
.expect(207)
})
it('GET /pm/id --> new projectmanager', () => {
request(api)
.get('/pm/' + pmOneId)
.expect(200)
.then((res) => {
expect(res.body.name).toBe(pmOne.name)
expect(res.body.tel).toBe(pmOne.tel)
})
})
And here is my app.js:
require('dotenv').config()
const express = require('express')
const mongoose = require('mongoose')
const app = express()
const pmRouter = require('./routes/pm.router')
// establish mongodb connection
var options = {
user: process.env.DATABASE_USER,
pass: process.env.DATABASE_PASSWD
}
mongoose.connect(process.env.DATABASE_URL, options)
const db = mongoose.connection
db.on('error', (error) => console.error(error))
app.use(express.json())
//app.use('/pm', pmRouter)
module.exports = app
The
//app.use('/pm', pmRouter)
Is commented out, so the route is not valid. That means all request should return 404 (which they do), so why am I getting passes on my tests:
> jest --forceExit --runInBand
PASS tests/projectmanager.test.js
Basic CRUD API
✓ GET /pm --> array of all projectmanagers (5 ms)
✓ GET /pm/id --> new projectmanager (3 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.571 s, estimated 1 s
Ran all test suites matching /tests\/projectmanager.test.js/i.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?
Supertest request returns a promise and if you want to assert the returned value it has to be awaited or returned explicitly.
Either return it or use async/await syntax.
In your case simply add a return statement.
it('GET /pm', () => {
return request(api).post('/pm').send(pmTwo).expect(207)
});

GET Method failing on Node.js Express backend application

I am trying to create a backend application using Node.js and Express to get post and modify user data from a .json file. Here is my app.js file:
const express = require("express");
const fs = require("fs");
//setting up the express router
const app = express();
app.use(express.json());
//write the code for routes here
app.post("/add", function(req, resp){
var jsonObject = req.body;
var jsonFile = fs.readFileSync("get.json", "UTF8");
var jsonArray = JSON.parse(jsonFile);
jsonArray.push(jsonObject);
jsonFile = JSON.stringify(jsonArray);
resp.json(jsonFile);
fs.writeFileSync("get.json",jsonFile,"utf-8");
});
app.get('/view/:id?', function(req, resp){
var queryURL = url.parse(req.url, true).query;
var jsonFile = fs.readFileSync("get.json", "UTF8");
var data = JSON.parse(jsonFile);
var id = req.params.id;
// if(typeof queryURL.id === "undefined" || queryURL.id == 0){
// resp.json(data);
// }else{
// resp.json(data[queryURL.id-1]);
// }
if (id >= 0) {
data.array.every(user => {
if (user.id === id) {
resp.json(user);
return false;
} else {
console.log("Next")
}
});
} else{
resp.json(data);
}
});
app.patch("/edit/:id", function(req, res){
let userID = req.params.id;
let userFile = fs.readFileSync("get.json", "UTF-8");
let userArray = JSON.parse(userFile);
let reqUserObject = req.body;
let newUserArray = userArray.map(user => {
if (user.id === userID) {
updatedUser = {...user, ...reqUserObject};
return updatedUser;
} else {
return user;
}
});
userFileData = JSON.stringify(newUserArray);
res.json(userFileData);
fs.writeFileSync("get.json", userFileData, "UTF-8");
});
module.exports = app;
All methods work except for
app.get('/view/:id?', function(req, resp)
This Method has an optional query parameter id, if you pass an id you get a user with an id equal to that id. If you dont pass in an id, you get all the users from get.json.
The json file (get.json) I used is:
[{"id":"1","name":"updated name","age":"22","gender":"Male","email":"userone#gmail.com"},{"id":"2","name":"user two","age":"24","gender":"Female","email":"usertwo#gmail.com"},{"id":"3","name":"user three","age":"23","gender":"Male","email":"userthree#gmail.com"},{"id":"4","name":"user four","age":"21","gender":"Male","email":"userfour#gmail.com"}]
And my test file is:
const request = require("supertest");
const app = require("../app");
const md5 = require("md5");
const fs = require("fs");
//updating a user profile
test("Updating a user", async () => {
await request(app)
.patch("/edit/1")
.send({
name: "updated name",
})
.expect(200);
setTimeout(() => {
const data = JSON.parse(fs.readFileSync("../post.json"));
expect(data.length).toBe(5);
expect(data[0].name).toBe("updated name");
}, 1000);
});
// // posting a data
test("Posting a new data", async () => {
await request(app)
.post("/add")
.send({
id: "5",
name: "user new",
age: "36",
gender: "Female",
email: "usernew#gmail.com",
})
.expect(200);
setTimeout(() => {
const data = JSON.parse(fs.readFileSync("../post.json"));
expect(data.length).toBe(5);
}, 1000);
});
//checking the get route
test("Getting all the user data", async () => {
const response = await request(app).get("/view").expect(200);
expect(response.body.length).toBe(4);
expect(md5(response.body)).toBe("f1d3ff8443297732862df21dc4e57262");
});
//getting profile of a user based on id
test("Getting a single user data", async () => {
const response = await request(app).get("/view?id=2").expect(200);
expect(response.body.length).toBe(1);
expect(md5(response.body)).toBe("93b885adfe0da089cdf634904fd59f71");
});
When I run the tests I get:
rm -rf ./test-report.xml && CI=true ./node_modules/.bin/jest
--testResultsProcessor ./node_modules/jest-junit-reporter --forceExit; t-reporter --forceExit;.bin/jest --testResultsProcessor
./node_modules/jest-juni FAIL test/app.test.js ✓ Updating a user
(45ms) ✓ Posting a new data (17ms) ✕ Getting all the user data
(10ms) ✕ Getting a single user data (4ms)
● Getting all the user data
expected 200 "OK", got 500 "Internal Server Error"
39 | //checking the get route
40 | test("Getting all the user data", async () => {
> 41 | const response = await request(app).get("/view").expect(200);
| ^
42 | expect(response.body.length).toBe(4);
43 | expect(md5(response.body)).toBe("f1d3ff8443297732862df21dc4e57262");
44 | });
at Object.<anonymous>.test (test/app.test.js:41:52)
----
at Test._assertStatus (node_modules/supertest/lib/test.js:252:14)
at node_modules/supertest/lib/test.js:306:17
at Test._assertFunction (node_modules/supertest/lib/test.js:285:13)
at Test.assert (node_modules/supertest/lib/test.js:164:23)
at Server.localAssert (node_modules/supertest/lib/test.js:120:14)
● Getting a single user data
expected 200 "OK", got 500 "Internal Server Error"
46 | //getting profile of a user based on id
47 | test("Getting a single user data", async () => {
> 48 | const response = await request(app).get("/view?id=2").expect(200);
| ^
49 | expect(response.body.length).toBe(1);
50 | expect(md5(response.body)).toBe("93b885adfe0da089cdf634904fd59f71");
51 | });
at Object.<anonymous>.test (test/app.test.js:48:57)
----
at Test._assertStatus (node_modules/supertest/lib/test.js:252:14)
at node_modules/supertest/lib/test.js:306:17
at Test._assertFunction (node_modules/supertest/lib/test.js:285:13)
at Test.assert (node_modules/supertest/lib/test.js:164:23)
at Server.localAssert (node_modules/supertest/lib/test.js:120:14)
Test Suites: 1 failed, 1 total Tests: 2 failed, 2 passed, 4
total Snapshots: 0 total Time: 1.612s Ran all test suites.
Force exiting Jest: Have you considered using --detectOpenHandles to
detect async operations that kept running after all tests finished?
We can see the last 2 tests ("Getting all the user data" and "Getting a single user data") did not pass. These 2 test the app.get('/view/:id?', function(req, resp) method.
How could I fix my app.get('/view/:id?', function(req, resp) method?
Update:
I fixed my app.js according to the suggestion of Yago Biermann, but it is still unable to pass the last 2 tests. My revised app.js:
const express = require("express");
const fs = require("fs");
//setting up the express router
const app = express();
app.use(express.json());
//write the code for routes here
app.post("/add", function(req, resp){
var jsonObject = req.body;
var jsonFile = fs.readFileSync("get.json", "UTF8");
var jsonArray = JSON.parse(jsonFile);
jsonArray.push(jsonObject);
jsonFile = JSON.stringify(jsonArray);
resp.json(jsonFile);
fs.writeFileSync("get.json",jsonFile,"utf-8");
});
app.get('/view', function(req, resp) {
const id = req.query.id;
var jsonFile = fs.readFileSync("get.json", "UTF8");
var data = JSON.parse(jsonFile);
// return the whole data if query parameter wasn't provided
if (!id) return resp.status(200).json(data)
// you should use find instead of every to get the user data
const user = data.find(user => {
if (user.id === id) {
return user;
};
return null;
});
// return the user otherwise return a 404 response
return user ? resp.status(200).json(user) : resp.status(404).json({message:"user not found"})
});
app.patch("/edit/:id", function(req, res){
let userID = req.params.id;
let userFile = fs.readFileSync("get.json", "UTF-8");
let userArray = JSON.parse(userFile);
let reqUserObject = req.body;
let newUserArray = userArray.map(user => {
if (user.id === userID) {
updatedUser = {...user, ...reqUserObject};
return updatedUser;
} else {
return user;
}
});
userFileData = JSON.stringify(newUserArray);
res.json(userFileData);
fs.writeFileSync("get.json", userFileData, "UTF-8");
});
module.exports = app;
And what I get when I run the tests:
rter --forceExit;es/.bin/jest --testResultsProcessor
./node_modules/jest-junit-repo FAIL test/app.test.js ✓ Updating a
user (43ms) ✓ Posting a new data (15ms) ✕ Getting all the user
data (9ms) ✕ Getting a single user data (4ms)
● Getting all the user data
expect(received).toBe(expected) // Object.is equality
Expected: 4
Received: 5
40 | test("Getting all the user data", async () => {
41 | const response = await request(app).get("/view").expect(200);
> 42 | expect(response.body.length).toBe(4);
| ^
43 | expect(md5(response.body)).toBe("f1d3ff8443297732862df21dc4e57262");
44 | });
45 |
at Object.<anonymous>.test (test/app.test.js:42:32)
● Getting a single user data
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: undefined
47 | test("Getting a single user data", async () => {
48 | const response = await request(app).get("/view?id=2").expect(200);
> 49 | expect(response.body.length).toBe(1);
| ^
50 | expect(md5(response.body)).toBe("93b885adfe0da089cdf634904fd59f71");
51 | });
52 |
at Object.<anonymous>.test (test/app.test.js:49:32)
Test Suites: 1 failed, 1 total Tests: 2 failed, 2 passed, 4
total Snapshots: 0 total Time: 1.71s Ran all test suites.
Force exiting Jest: Have you considered using --detectOpenHandles to
detect async operations that kept running after all tests finished?
The problem seems to be at your tests, in your test "Getting all the user data" you are making a request to /view but you don't have any route to view, at least it's not in your question, note that /view/:id is not the same as /view. Lastly, in your test "Getting a single user data" you are not providing the id for the user as a url parameter, instead you are passing it as a query parameter, so try to do the following:
test("Getting a single user data", async () => {
// pass the id as url
const response = await request(app).get("/view/2").expect(200);
expect(response.params.id).toBe(2);
expect(response.body.length).toBe(1);
expect(md5(response.body)).toBe("93b885adfe0da089cdf634904fd59f71");
});
See the docs about req.params. Hope I helped a bit!
Edit: As you said that the test file can't be changed, do the following in your view route:
// Change to view, now the test on route /view should work
app.get('/view', function(req, resp) {
const id = req.query.id;
var jsonFile = fs.readFileSync("get.json", "UTF8");
var data = JSON.parse(jsonFile);
// return the whole data if query parameter wasn't provided
if (!id) return resp.status(200).json(data)
// you should use find instead of every to get the user data
const user = data.find(user => {
if (user.id === id) {
return user;
};
return null;
});
// return the user otherwise return a 404 response
return user ? resp.status(200).json([user]) : resp.status(404).json({message:"user not found"})
});

Having trouble stubbing an asynchronous function

I am currently working on a full-stack application using the MERN stack. I have been writing unit tests for each route's request handler, and have been finding some difficulty testing error cases, particularly in trying to stub a function to reject a promise
I have the relevant code shown below:
One of my endpoints. Request handling is delegated to userController
const express = require("express");
const { body } = require("express-validator");
const router = express.Router();
const userController = require("../../controllers/user");
router.post(
"/",
body("username")
.isLength({
min: 3,
max: 30,
})
.withMessage(
"Your username must be at least 3 characters and no more than 30!"
),
body("password")
.isLength({ min: 3, max: 50 })
.withMessage(
"Your password must be at least 3 characters and no more than 50!"
),
userController.createNewUser
);
The request handler for the above endpoint. I am trying to test createNewUser. I want to stub createNewUser so that it causes an error to be thrown, so I can test that a 500 status code response is sent.
const bcrypt = require("bcryptjs");
const { validationResult } = require("express-validator");
const User = require("../models/User");
exports.createNewUser = async (req, res, next) => {
const { username, password } = req.body;
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array(),
});
}
try {
// Create a bcrypt salt
const salt = await bcrypt.genSalt(12);
// Hash the password
const hashedPassword = await bcrypt.hash(password, salt);
// Create a new user
const user = new User({
username,
password: hashedPassword,
});
const response = await user.save();
res.status(200).json(response);
} catch (err) {
res.status(500).json({ msg: err.message });
}
};
The unit test for User endpoints. I am unsure how to test the error case where a 500 status code is returned...
const request = require("supertest");
// const todosController = require("../controllers/todos");
const server = require("../server");
const User = require("../models/TodoItem");
const db = require("./db");
const agent = request.agent(server);
// Setup connection to the database
beforeAll(async () => await db.connect());
afterEach(async () => await db.clear());
afterAll(async () => await db.close());
describe("User endpoints test suite", () => {
describe("POST api/user", () => {
test("It should create a user successfully and return a 200 response code", async () => {
const response = await agent
.post("/api/user")
.set("content-type", "application/json")
.send({ username: "Bob", password: "12345" });
expect(response.body.username).toEqual("Bob");
expect(response.status).toBe(200);
});
});
});
When you are creating unit test, create something small first, you can add complexity and refactor later.
Below are example simple unit and integration tests based on your code.
You can start with user controller.
// File: user.controller.js
const bcrypt = require('bcryptjs');
exports.createNewUser = async (req, res) => {
try {
// Create a bcrypt salt.
const salt = await bcrypt.genSalt(12);
// Just make it simple, show the salt.
res.status(200).json(salt);
} catch (err) {
// Other wise, return the error message.
res.status(500).json({ msg: err.message });
}
};
Based on that try and catch, you can create unit test.
// File: user.controller.spec.js
const bcrypt = require('bcryptjs');
const user = require('./user.controller');
describe('User Controller', () => {
describe('create New User', () => {
const fakeJson = jest.fn();
const fakeStatus = jest.fn().mockReturnThis();
const fakeRes = {
status: fakeStatus,
json: fakeJson,
};
const spy = jest.spyOn(bcrypt, 'genSalt');
afterEach(() => {
jest.clearAllMocks();
});
it('should return salt', async () => {
const testSalt = 'salt';
// Mock the bcrypt.genSalt, always resolved with value testSalt.
spy.mockResolvedValue(testSalt);
// Call the function under test.
await user.createNewUser(undefined, fakeRes);
// Set the expectations.
expect(fakeStatus).toHaveBeenCalledWith(200);
expect(fakeJson).toHaveBeenCalledWith(testSalt);
expect(spy.mock.calls[0][0]).toBe(12);
});
it('should return error message when error', async () => {
const error = new Error('XXX');
// Mock the bcrypt.genSalt, always resolved with value testSalt.
spy.mockRejectedValue(error);
// Call the function under test.
await user.createNewUser(undefined, fakeRes);
// Set the expectations.
expect(fakeStatus).toHaveBeenCalledWith(500);
expect(fakeJson).toHaveBeenCalledWith({ msg: error.message });
expect(spy.mock.calls[0][0]).toBe(12);
});
});
});
When you run it on terminal:
$ npx jest user.controller.spec.js
PASS ./user.controller.spec.js
User Controller
create New User
✓ should return salt (5 ms)
✓ should return error message when error (1 ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
user.controller.js | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.511 s, estimated 1 s
Ran all test suites matching /user.controller.spec.js/i.
Next, if you have sure with your controller, you can create integration test with express.
For example you create app index like this.
// File: index.js
const express = require('express');
const userController = require('./user.controller');
const router = express.Router();
router.post('/user', (req, res, next) => userController.createNewUser(req, res, next));
const app = express();
app.use('/api', router);
module.exports = app;
You can test it using jest for normal & error case like this.
// File: index.spec.js
const request = require('supertest');
const bcrypt = require('bcryptjs');
const server = require('./index');
const userController = require('./user.controller');
const agent = request.agent(server);
describe('App', () => {
describe('POST /', () => {
// Create spy on bcrypt.
const spy = jest.spyOn(bcrypt, 'genSalt');
const error = new Error('XXX');
afterEach(() => {
jest.clearAllMocks();
});
it('should create a salt successfully and return a 200 response code', async () => {
// This test is slow because directly call bcrypt.genSalt.
// To make it faster, mock bcrypt completely, or use spy.mockResolvedValue('SALT');
// Send post request.
const response = await agent.post('/api/user');
// Make sure the response.
expect(response.status).toBe(200);
expect(response.type).toBe('application/json');
expect(spy.mock.results[0].value).toBeDefined();
const spyResult = await spy.mock.results[0].value;
expect(response.body).toBe(spyResult)
});
it('should return 500 and error message when catch error', async () => {
// Makesure spy reject.
spy.mockRejectedValue(error);
// Send post request.
const response = await agent.post('/api/user');
// Make sure the response.
expect(response.status).toBe(500);
expect(response.type).toBe('application/json');
expect(response.body).toBeDefined();
expect(response.body.msg).toBeDefined();
expect(response.body.msg).toBe(error.message);
});
// Or play around with another spy to error alternatives.
it('should return 404 when pass to next', async () => {
// Makesure createNewUser error.
jest.spyOn(userController, 'createNewUser').mockImplementation((req, res, next) => {
// You can setup res here or other implementation to check.
// For example, do next.
next();
});
// Send post request.
const response = await agent.post('/api/user');
// Make sure the response.
expect(response.status).toBe(404);
// Method bcrypt.genSalt should not get called.
expect(spy).not.toHaveBeenCalled();
});
});
});
When you run it from terminal:
$ npx jest index.spec.js
PASS ./index.spec.js
App
POST /
✓ should create a salt successfully and return a 200 response code (40 ms)
✓ should return 500 and error message when catch error (4 ms)
✓ should return 404 when pass to next (5 ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.js | 100 | 100 | 100 | 100 |
user.controller.js | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.809 s, estimated 1 s
Ran all test suites matching /index.spec.js/i.
Note: You do not need to use sinon, jest provides mock functions.

Flakey tests when testing firebase functions using Jest

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)

Firebase Stripe (Error) Promises must be handled appropriately

I am trying to process my payment with firebase and stripe and have come across a problem when trying to deploy my function to the cloud saying 'Promises must be handled appropriately. I know this is a tlint compilation error but can't figure out why the error is being triggered.
Here is my code
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
const stripe = require('stripe')(functions.config().stripe.testkey);
exports.stripeCharge = functions.firestore
.document('/payments/{userId}/mypayments/{paymentId}')
.onCreate((snap,event) => {
const payment = snap.data()
const userId = event.params.userId
const paymentId = event.params.paymentId
// checks if payment exists or if it has already been charged
if (!payment || payment.charge) return null;
return admin.firestore()
.doc(`/users/${userId}`)
.get()
.then(snapshot => {
return snapshot
})
.then(customer => {
const amount = payment.price * 100 // amount must be in cents
const idempotency_key = paymentId // prevent duplicate charges
const source = payment.token.id
const currency = 'usd'
const charge = {amount, currency, source}
return stripe.charges.create(charge, { idempotency_key })
})
.then((charge) => {
admin.firestore()//The error keeps referring me to this line
.collection('/payments').doc(userId).collection('mypayments').doc(paymentId)
.set({
charge: charge
}, { merge: true })
})
})
The line generating the error is stated above
Actually, with the latest version(s) of Cloud Functions you are not obliged to include a catch() in your Promises chaining. The platform where the Cloud Function runs will handle the error itself.
Based on this post What could this be about? [TsLint Error: "Promises must be handled appropriately"] it is apparently an error generated by TsLint (EsLint?).
However, independently of this "error" detected by TsLint, I think you may encounter problems with your Cloud Function because you don't return the last promise of your chain:
return admin.firestore() //HERE YOU RETURN CORRECTLY
.doc(`/users/${userId}`)
.get()
.then(snapshot => {
return snapshot //HERE YOU RETURN CORRECTLY
})
.then(customer => {
const amount = payment.price * 100 // amount must be in cents
const idempotency_key = paymentId // prevent duplicate charges
const source = payment.token.id
const currency = 'usd'
const charge = {amount, currency, source}
return stripe.charges.create(charge, { idempotency_key }) //HERE YOU RETURN CORRECTLY
})
.then((charge) => {
return admin.firestore() //HERE, IN YOUR CODE, YOU DON'T RETURN
.collection('/payments').doc(userId).collection('mypayments').doc(paymentId)
.set({
charge: charge
}, { merge: true })
})
})
finally figure it out
Whenever you make a promise function, it has to end with an error handler so i fixed this by using a simple catch
.then((charge) => {
admin.firestore()
.collection('/payments').doc(userId).collection('mypayments').doc(paymentId)
.set({
charge: charge
}, { merge: true })
.catch(er=>{
console.log(er);
return er
}
)
})

Resources