Mocking Firebase function with Jest (Unit testing) - node.js

I am creating unit tests, but I can find a way to mock firebase functions and spesify the return type when I call them. Below I posted the what I want to mock(account.service.ts) and what test I currenlty have. I want to mock and specify what is to be returned with admin... (aka set the resp object value).
import * as admin from 'firebase-admin';
account.service.ts
const resp = admin
.auth()
.createUser({
email: registerDto.email,
emailVerified: false,
password: registerDto.password,
displayName: registerDto.displayName,
disabled: false,
})
.then(
(userCredential): Account => ({
uid: userCredential.uid,
email: userCredential.email,
emailVerified: userCredential.emailVerified,
displayName: userCredential.displayName,
message: 'User is successfully registered!',
}),
)
.catch((error) => {
// eslint-disable-next-line max-len
throw new HttpException(`${'Bad Request Error creating new user: '}${error.message}`, HttpStatus.BAD_REQUEST);
});
account.service.spec.ts
describe('61396089', () => {
afterEach(() => {
jest.restoreAllMocks();
});
const obj = {
uid: 'uid',
email: 'email',
emailVerified: 'emailVerified',
displayName: 'displayName',
message: 'User is successfully registered!',
};
const jestAdminMock = {
admin: () => ({
auth: () => ({
createUser: () => ({
then: () => ({
catch: () => obj,
}),
}),
}),
}),
};
it('should pass', () => {
const mockDataTable = {
admin: jest.fn().mockReturnThis(),
auth: jest.fn().mockReturnThis(),
createUser: jest.fn().mockReturnThis(),
then: jest.fn().mockReturnThis(),
catch: jest.fn().mockReturnValueOnce(obj),
};
jest.spyOn(jestAdminMock, 'admin').mockImplementationOnce(() => mockDataTable);
const actual = service.registerUser(registerDTO);
expect(actual).toBe(obj);
});
});
});

I managed to make a test like this example
This is the function
import admin from 'firebase-admin'
admin.initializeApp({
credential: admin.credential.cert(credentials),
})
const createClosure = (admin) => {
if (!admin) {
throw new Error(Errors.FIREBASE_ADMIN_SDK_NOT_PROVIDED)
}
return (data) => {
if (
data &&
!Array.isArray(data) &&
typeof data === 'object' &&
Object.keys(data).length > 0
) {
const { firstName, lastName } = data
const displayName = `${firstName} ${lastName}`
return admin.auth().createUser({ ...data, displayName })
}
throw new Error(Errors.INVALID_DATA)
}
}
/*
.....
*/
const create = createClosure(admin)
export { create, createClosure }
This is a test example
import { createClosure } from "../path/to/function"
describe('createClosure', () => {
it('should be a function', () => {
expect(typeof createClosure).toBe('function')
})
describe('when admin is not provided', () => {
it('should throw "Firebase Admin SDK not provided"', () => {
const expected = Errors.FIREBASE_ADMIN_SDK_NOT_PROVIDED
expect(() => createClosure()).toThrow(expected)
})
})
describe('when admin is provided', () => {
describe('when data is invalid', () => {
const createUser = jest.fn()
const admin = {
auth: () => ({
createUser,
}),
}
const data1 = 123
const data2 = 'hello'
const data3 = ['a', 'b', 'c']
const data4 = {}
it('should throw "Invalid data"', () => {
expect(() => createClosure(admin)()).toThrow(Errors.INVALID_DATA)
expect(() => createClosure(admin)(data1)).toThrow(Errors.INVALID_DATA)
expect(() => createClosure(admin)(data2)).toThrow(Errors.INVALID_DATA)
expect(() => createClosure(admin)(data3)).toThrow(Errors.INVALID_DATA)
expect(() => createClosure(admin)(data4)).toThrow(Errors.INVALID_DATA)
})
})
describe('when data is valid', () => {
const data = {
firstName: 'Alice',
lastName: 'Alley',
foo: 'bar',
baz: {
boo: 'bii',
buu: 'byy',
},
}
describe('when createUser rejects', () => {
const e = new Error('Error happened!')
const createUser = jest.fn().mockRejectedValue(e)
const admin = {
auth: () => ({
createUser,
}),
}
const create = createClosure(admin)
it('should call createUser once', async () => {
try {
await createUser(data)
} catch (error) {}
expect(createUser).toBeCalledTimes(1)
expect(createUser).toBeCalledWith({ ...data })
})
it('should reject', async () => {
await expect(create(data)).rejects.toEqual(e)
})
})
describe('when save resolves', () => {
const expected = {
baz: { boo: 'bii', buu: 'byy' },
displayName: 'Alice Alley',
lastName: 'Alley',
}
const displayName = `${data.firstName} ${data.lastName}`
const createUser = jest.fn().mockResolvedValue(expected)
const admin = {
auth: () => ({
createUser,
}),
}
const create = createClosure(admin)
it('should call save once', async () => {
try {
await create(data)
} catch (error) {}
expect(createUser).toBeCalledTimes(1)
expect(createUser).toBeCalledWith({ ...data, displayName })
})
it('should resolve', async () => {
const result = await create(data)
expect(result).toMatchObject(expected)
})
})
})
})
})

Related

"Bad request" error when using params in supertest and jest

I have the following controller for updating project which gets id from param.
const updateProject = async (req: Request, res: Response, next: NextFunction) => {
const { title, desc } = req.body;
const { projectID } = req.params;
const { user } = <any>req;
if (!isValidObjectId(projectID)) {
return next(new ErrorResponse("Invalid project ID", 400));
}
try {
const project = await Project.findOneAndUpdate(
{ _id: projectID, user: user._id },
{
title: title,
desc: desc,
},
{ new: true }
);
if (!project) {
return next(new ErrorResponse("Project not found", 404));
}
res.status(200).json(project);
} catch (err) {
next(err);
}
};
I've written a test for this controller but I keep getting "Bad Request". But when I test it manually using Postman, it works fine. And other tests that do not require params work fine too.
import mongoose from "mongoose";
const projectOneId = new mongoose.Types.ObjectId();
export const projectOne = {
_id: projectOneId,
title: "First Project",
desc: "This is the first project",
user: userOneId,
};
describe("/api/projects", () => {
//login using cookie before running the test
let cookie: string;
beforeEach(async () => {
await request(app)
.post("/api/auth/login")
.send(userOne)
.expect(200)
.then((res) => {
const cookies = res.headers["set-cookie"][0]
.split(",")
.map((item: string) => item.split(";")[0]);
cookie = cookies.join(";");
});
});
describe("PUT /api/projects/:projectID", () => {
it("Should update a project", async () => {
await request(app)
.put(`/api/projects/:projectID`)
.set("Cookie", cookie)
.query({ projectID: projectOne._id.toString() })
.send(projectOne)
.expect(200);
});
});

How to add Auth Custom claims

Firebase function
I am trying to set my user role to admin using a callable function:
export const addAdminRole = functions.https.onCall(async (data, context) => {
admin.auth().setCustomUserClaims(data.uid, {
admin: true,
seller: false,
});
});
Cient
And here is how I am calling the function on the client:
const register = (email: string, password: string) => {
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
const addAdminRole = httpsCallable(functions, "addAdminRole");
addAdminRole({ email: user.email, uid: user.uid })
.then((result) => {
console.log(result);
})
.catch((error) => console.log(error));
history.push(`/home/${user.uid}`);
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
// ..
});
};
The user is created but, my Admin role is not added
The problem may come from the fact that you don't correctly handle the promise returned by the setCustomUserClaims() method in your Cloud Function and therefore the Cloud Function platform may clean up you CF before it reaches its terminating state. Correctly managing the life-cycle of your Cloud Function is key, as explained here in the doc.
The following should solve the problem:
export const addAdminRole = functions.https.onCall(async (data, context) => {
try {
await admin.auth().setCustomUserClaims(data.uid, {
admin: true,
seller: false,
});
return {result: "Success"}
} catch (error) {
// See https://firebase.google.com/docs/functions/callable#handle_errors
}
});
In addition, you can refactor your front-end code as follows to correctly chain the promises:
const register = (email: string, password: string) => {
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
const addAdminRole = httpsCallable(functions, "addAdminRole");
return addAdminRole({ email: user.email, uid: user.uid });
})
.then((result) => {
console.log(result);
history.push(`/home/${user.uid}`);
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
// ..
});
};

MongoParseError: URI does not have hostname, domain name and tld only when testing the methods

I am trying to test my MongoDB library. I want to verify if a variable is an instance of my library and if the method is called correctly. I am using jest to make my tests.
import { MongoLib } from '../index'
let database: any
beforeAll(() => {
database = new MongoLib({
hostname: 'localhost',
database: 'test',
password: encodeURI('password'),
username: 'anothertest'
})
})
afterAll(() => {
database = null
})
describe('Initializing class', () => {
test('isInstanceOf', () => {
expect(database).toBeInstanceOf(MongoLib)
})
})
describe('Methods#ToBeThruty', () => {
test('getAllToBeThruty', async () => {
try {
const users = await database.getAll('users')
expect(users).toBeTruthy()
} catch (error) {
console.error(error)
}
})
test('getToBeThruty', async () => {
try {
const user = await database.get('users', '60e13de94a37e1c6d0174749')
expect(user).toBeTruthy()
} catch (error) {
console.error(error)
}
})
test('createToBeThruty', async () => {
try {
const user = await database.create('users', { user: 'hello', email: 'world' })
expect(user).toBeTruthy()
} catch (error) {
console.error(error)
}
})
test('updateToBeThruty', async () => {
try {
const user = await database.update('users', '60e13de94a37e1c6d0174749', { user: 'world', email: 'hello' })
expect(user).toBeTruthy()
} catch (error) {
console.error(error)
}
})
test('replaceToBeThruty', async () => {
try {
const user = await database.replace('users', '60e13de94a37e1c6d0174749', { user: 'world', email: 'hello' })
expect(user).toBeTruthy()
} catch (error) {
console.error(error)
}
})
test('deleteToBeThruty', async () => {
try {
const user = await database.delete('users', '60e13de94a37e1c6d0174749')
expect(user).toBeTruthy()
} catch (error) {
console.error(error)
}
})
})
The test is passing, but after that, I am getting this error:
MongoParseError: URI does not have hostname, domain name and tld
at parseSrvConnectionString
(/Users/diesanromero/Documents/KemusCorp/OpenSource/mongodb-
crud/node_modules/mongodb/lib/core/uri_parser.js:51:21)
at MongoLib.Object..MongoLib.connect
(/Users/diesanromero/Documents/KemusCorp/OpenSource/mongodb-
crud/src/index.ts:25:29)
at MongoLib.
(/Users/diesanromero/Documents/KemusCorp/OpenSource/mongodb-
crud/src/index.ts:40:17)
The code for MongoLib is the following:
import { MongoClient, ObjectId } from 'mongodb'
import Settigns from "./settings"
export class MongoLib {
private credentials: string
private database: string
private host: string
private options: object
private client: MongoClient
private url: string
private static connection: any
constructor(settings: Settigns) {
this.credentials = `${settings.username}:${settings.password}`
this.host = `${settings.hostname}:${settings.port}`
this.database = settings.database
this.url = `mongodb+srv://${this.credentials}#${this.host}/${settings.database}?retryWrites=true&w=majority`
this.options = { useUnifiedTopology: true }
this.client = new MongoClient(this.url, this.options)
}
private connect () {
if (!MongoLib.connection) {
MongoLib.connection = new Promise((resolve, reject) => {
this.client.connect((error: Error) => {
if (error) reject(error)
else {
resolve(this.client.db(this.database))
console.log('Connected successfully to MongoDB.')
}
})
})
}
return MongoLib.connection
}
public async getAll (collection: string, query?: object) {
return this.connect()
.then((db: any) => {
return db.collection(collection).find(query).toArray()
})
}
public async get (collection: string, id: string) {
return this.connect()
.then((db:any) => {
return db.collection(collection).findOne({ _id: new ObjectId(id) })
})
}
public async create (collection: string, data: object) {
return this.connect()
.then((db: any) => {
return db.collection(collection).insertOne(data)
})
.then(() => 'Data inserted')
}
public async update (collection: string, id: string, data: object) {
return this.connect()
.then((db: any) => {
return db.collection(collection).updateOne({ _id: new ObjectId(id) }, { $set: data }, { upsert: true })
})
.then(() => 'Data updated')
}
public async replace (collection: string, id: string, data: object) {
return this.connect()
.then((db: any) => {
return db.collection(collection).replaceOne({ _id: new ObjectId(id) }, data, { upsert: true })
})
.then(() => 'Data replaced')
}
public async delete (collection: string, id: string) {
return this.connect()
.then((db: any) => {
return db.collection(collection).deleteOne({ _id: new ObjectId(id) })
})
.then(() => 'Data deleted')
}
}
Take in mind that settings.js is an exported interface with the basic configuration.

The loader.load() function must be called with a value,but got: undefined

I am following this graphql tutorial, everything was going ok until I try to use dataloaders.
My server.js is:
const start = async () => {
const mongo = await connectMongo();
const buildOptions = async req => {
const user = await authenticate(req, mongo.Users);
return {
context: {
dataloaders: buildDataloaders(mongo),
mongo,
user
},
schema
};
};
app.use('/graphql', bodyParser.json(), graphqlExpress(buildOptions));
app.use(
'/graphiql',
graphiqlExpress({
endpointURL: '/graphql',
passHeader: `'Authorization': 'bearer token-name#email.com'`
})
);
app.use('/', expressStaticGzip('dist'));
app.use('/attendance', expressStaticGzip('dist'));
app.use('/login', expressStaticGzip('dist'));
spdy.createServer(sslOptions, app).listen(process.env.PORT || 8080, error => {
if (error) {
console.error(error);
return process.exit(1);
} else {
console.info(
`App available at https://localhost:${process.env.PORT || 3000}`
);
}
});
};
My copy and paste dataloaders.js:
const DataLoader = require('dataloader');
async function batchUsers(Users, keys) {
return await Users.find({ _id: { $in: keys } }).toArray();
}
module.exports = ({ Users }) => ({
userLoader: new DataLoader(keys => batchUsers(Users, keys), {
cacheKeyFn: key => key.toString()
})
});
And my resolvers.js:
export default {
Query: {
allLinks: async (root, data, { mongo: { Links } }) =>
Links.find({}).toArray()
},
Mutation: {
createLink: async (root, data, { mongo: { Links }, user }) => {
const newLink = Object.assign({ postedById: user && user._id }, data);
const response = await Links.insert(newLink);
return Object.assign({ id: response.insertedIds[0] }, newLink);
},
createUser: async (root, data, { mongo: { Users } }) => {
const newUser = {
name: data.name,
email: data.authProvider.email.email,
password: data.authProvider.email.password
};
const response = await Users.insert(newUser);
return Object.assign({ id: response.insertedIds[0] }, newUser);
},
signinUser: async (root, data, { mongo: { Users } }) => {
const user = await Users.findOne({ email: data.email.email });
if (data.email.password === user.password) {
return { token: `token-${user.email}`, user };
}
}
},
Link: {
id: root => root._id || root.id,
postedBy: async ({ postedById }, data, { dataloaders: { userLoader } }) => {
return await userLoader.load(postedById);
}
},
User: {
id: root => root._id || root.id
}
};
When I try get my allLinks I got the error:
TypeError: The loader.load() function must be called with a value,but
got: undefined.
Can anyone help me?
So I was able to reproduce the error by creating a link with a user, deleting the user from the Mongo database, and then querying for the postedBy attribute of the Link.
I would suggest dropping all your links and recreating your user (register + sign in), creating a new link, then querying for the postedBy field.

Graphql Error undefined adding a relationship

I have the following structure. Every Account can have one security type.
So its a one-to-many from SecurityType to Account.
Everything works using the code
File: AccountSchema.js
const SecurityType = require('./LookupSchema').SecurityType;
console.log(Account);
const Account = new GraphQLObjectType({
name: 'Account',
description: 'Account access',
fields: () =>
({
id: {
type: GraphQLString
},
security_type:
{
type: SecurityType,
resolve(parent, args, ast){
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [parent.security_type_id];
db.query(db.connection, `SELECT * FROM lookups.security_type WHERE id = $1`, parameters)
.then(result =>
{
resolve(result.entrys.rows[0]);
})
.catch(err =>
{
reject(err.message);
});
});
}
}
})
});
module.exports = {
Account : Account
}
File: LookupSchema.js
const Account = require('./AccountSchema').Account;
console.log(Account);
const SecurityType = new GraphQLObjectType({
name: 'SecurityType',
description: 'Used to for specifying security type',
fields: () =>
({
id: {
type: GraphQLString
}
})
});
module.exports = {
SecurityType: SecurityType
}
File: Query.js
const Query = new GraphQLObjectType({
name: 'Query',
description: 'Root query object',
fields: () => ({
accounts: {
type: new GraphQLList(Account),
resolve(root, args, ast) {
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [];
db.query(db.connection, `SELECT * FROM accounts.account`, parameters)
.then(result =>
{
console.log(result);
resolve(result.entrys.rows);
})
.catch(err =>
{
console.log(err);
reject(err.message);
});
});
}
},
securityTypes: {
type: new GraphQLList(SecurityType),
resolve(root){
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [];
db.query(db.connection, `SELECT * FROM lookups.security_type`, parameters)
.then(result =>
{
resolve(result.entrys.rows);
})
.catch(err =>
{
reject(err.message);
});
});
}
}
})
});
The problem I have is when I add to the file LookupSchema.js the accounts
const SecurityType = new GraphQLObjectType({
name: 'SecurityType',
description: 'Used to for specifying security type',
fields: () =>
({
id: {
type: GraphQLString
},
accounts: {
type: new GraphQLList(Account),
resolve(parent, args, ast){
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [parent.id];
db.query(db.connection, `SELECT * FROM accounts.account WHERE security_type_id = $1`, parameters)
.then(result =>
{
resolve(result.entrys.rows);
})
.catch(err =>
{
reject(err.message);
});
});
}
}
})
});
I get the following error when I start the service
Error: Can only create List of a GraphQLType but got: undefined.
I put console.log for each Account and SecurityType to check for the import and I noticed in LookupSchema, Account is undefined. I did some research and this might be a circular issue but not quite sure a solution for it.
Any advise would be appreciated
To avoide the Cyclic Problem you can use the require function inside the fields() function.
So, Inside AccountSchema.js fields:() function will first import the SecurityType then only we will be using the the other fields with the return {}, same for other files.
AccountSchema.js
const {
GraphQLObjectType,
GraphQLString,
} = require('graphql');
const Account = new GraphQLObjectType({
name: 'Account',
description: 'Account access',
fields: () => {
const SecurityType = require('./LookUpSchema');
return {
id: {
type: GraphQLString,
},
security_type:
{
type: SecurityType,
resolve(parent, args, ast) {
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [parent.security_type_id];
db.query(db.connection, 'SELECT * FROM lookups.security_type WHERE id = $1', parameters)
.then((result) => {
resolve(result.entrys.rows[0]);
})
.catch((err) => {
reject(err.message);
});
});
},
},
};
},
});
module.exports = Account;
LookUpSchema.js
const {
GraphQLObjectType,
GraphQLString,
GraphQLList,
} = require('graphql');
const SecurityType = new GraphQLObjectType({
name: 'SecurityType',
description: 'Used to for specifying security type',
fields: () => {
const Account = require('./AccountSchema');
return {
id: {
type: GraphQLString,
},
accounts: {
type: new GraphQLList(Account),
resolve(parent, args, ast) {
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [parent.id];
db.query(db.connection, 'SELECT * FROM accounts.account WHERE security_type_id = $1', parameters)
.then((result) => {
resolve(result.entrys.rows);
})
.catch((err) => {
reject(err.message);
});
});
},
},
};
},
});
module.exports = SecurityType;
Query.js
const {
GraphQLList,
GraphQLObjectType,
GraphQLSchema,
} = require('graphql');
const Account = require('./AccountSchema');
const SecurityType = require('./LookUpSchema');
console.log('Account', Account);
const Query = new GraphQLObjectType({
name: 'Query',
description: 'Root query object',
fields: () => ({
accounts: {
type: new GraphQLList(Account),
resolve(root, args, ast) {
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [];
db.query(db.connection, 'SELECT * FROM accounts.account', parameters)
.then((result) => {
console.log(result);
resolve(result.entrys.rows);
})
.catch((err) => {
console.log(err);
reject(err.message);
});
});
},
},
securityTypes: {
type: new GraphQLList(SecurityType),
resolve(root) {
return new Promise((resolve, reject) => {
const db = ast.db;
const parameters = [];
db.query(db.connection, 'SELECT * FROM lookups.security_type', parameters)
.then((result) => {
resolve(result.entrys.rows);
})
.catch((err) => {
reject(err.message);
});
});
},
},
}),
});
const schema = new GraphQLSchema({
query: Query,
// mutation: MutationType,
});
module.exports = schema;
GraphiQL

Resources