I'm trying to write some integration tests on NodeJS with Firebase (firebase-admin, with the test library jest and supertest), and some tests are failing randomly when I run all my tests. Separately, my tests are passing, but it seems like when too many test cases are running, some api calls are failing. Does someone here already had such problem? What are the solutions for this problem? What could cause this problem? (NB: I run my tests sequentially for not mixing up the initialization of my database. I use the option --runInBand with jest)
There are some mocking libraries available, but it seems like they work with the old api of firebase.
Another solution would be to mock all my functions that manipulate firebase, but I won't have a "real" integration test anymore, and it means doing a lot of extra coding for writing those mock. Is it a best practice to do so?
Thank you in advance!
EDIT: code snippet:
initTest.js:
const request = require('supertest');
const net = require('net');
const app = require('../src/server').default;
export const initServer = () => {
const server = net.createServer(function(sock) {
sock.end('Hello world\n');
});
return server
}
export const createAdminAndReturnToken = async (password) => {
await request(app.callback())
.post('/admin/users/sa')
.set('auth','')
.send({password});
// user logs in
const res = await request(app.callback())
.post('/web/users/login')
.set('auth','')
.send({email:"sa#optimetriks.com",password})
return res.body.data.token;
}
utils.ts:
import firestore from "../../src/tools/firestore/index";
export async function execOperations(operations,action,obj) {
if (process.env.NODE_ENV === "test") {
await Promise.all(operations)
.then(() => {
console.log(action+" "+obj+" in database");
})
.catch(() => {
console.log("Error", "error while "+action+"ing "+obj+" to database");
});
} else {
console.log(
"Error",
"cannot execute this action outside from the test environment"
);
}
}
//////////////////////// Delete collections ////////////////////////
export async function deleteAllCollections() {
const collections = ["clients", "web_users","clients_web_users","clients_app_users","app_users"];
collections.forEach(collection => {
deleteCollection(collection);
});
}
export async function deleteCollection(collectionPath) {
const batchSize = 10;
var collectionRef = firestore.collection(collectionPath);
var query = collectionRef.orderBy("__name__").limit(batchSize);
return await new Promise((resolve, reject) => {
deleteQueryBatch(firestore, query, batchSize, resolve, reject);
});
}
async function deleteQueryBatch(firestore, query, batchSize, resolve, reject) {
query
.get()
.then(snapshot => {
// When there are no documents left, we are done
if (snapshot.size == 0) {
return 0;
}
// Delete documents in a batch
var batch = firestore.batch();
snapshot.docs.forEach(doc => {
batch.delete(doc.ref);
});
return batch.commit().then(() => {
return snapshot.size;
});
})
.then(numDeleted => {
if (numDeleted === 0) {
resolve();
return;
}
// Recurse on the next process tick, to avoid
// exploding the stack.
process.nextTick(() => {
deleteQueryBatch(firestore, query, batchSize, resolve, reject);
});
})
.catch(reject);
}
populateClient.ts:
import firestore from "../../src/tools/firestore/index";
import {execOperations} from "./utils";
import { generateClientData } from "../factory/clientFactory";
jest.setTimeout(10000); // some actions here needs more than the standard 5s timeout of jest
// CLIENT
export async function addClient(client) {
const clientData = await generateClientData(client);
await firestore
.collection("clients")
.doc(clientData.id)
.set(clientData)
}
export async function addClients(clientNb) {
let operations = [];
for (let i = 0; i < clientNb; i++) {
const clientData = await generateClientData({});
operations.push(
await firestore
.collection("clients")
.doc(clientData.id)
.set(clientData)
);
}
await execOperations(operations,"add","client");
}
retrieveClient.ts:
import firestore from "../../src/tools/firestore/index";
import { resolveSnapshotData } from "../../src/tools/tools";
export async function getAllClients() {
return new Promise((resolve, reject) => {
firestore
.collection("clients")
.get()
.then(data => {
resolveSnapshotData(data, resolve);
})
.catch(err => reject(err));
});
}
clients.test.js:
const request = require('supertest');
const app = require('../../../src/server').default;
const {deleteAllCollections, deleteCollection} = require('../../../__utils__/populate/utils')
const {addClient} = require('../../../__utils__/populate/populateClient')
const {getAllClients} = require('../../../__utils__/retrieve/retrieveClient')
const {initServer,createAdminAndReturnToken} = require('../../../__utils__/initTest');
const faker = require('faker');
let token_admin;
let _server;
// for simplicity, we use the same password for every users
const password = "secretpassword";
beforeAll(async () => {
_server = initServer(); // start
await deleteAllCollections()
// create a super admin, login and store the token
token_admin = await createAdminAndReturnToken(password);
_server.close(); // stop
})
afterAll(async () => {
// remove the users created during the campaign
_server = initServer(); // start
await deleteAllCollections()
_server.close(); // stop
})
describe('Manage client', () => {
beforeEach(() => {
_server = initServer(); // start
})
afterEach(async () => {
await deleteCollection("clients")
_server.close(); // stop
})
describe('Get All clients', () => {
const exec = (token) => {
return request(app.callback())
.get('/clients')
.set('auth',token)
}
it('should return a 200 when super admin provide the action', async () => {
const res = await exec(token_admin);
expect(res.status).toBe(200);
});
it('should contain an empty array while no client registered', async () => {
const res = await exec(token_admin);
expect(res.body.data.clients).toEqual([]);
});
it('should contain an array with one item while a client is registered', async () => {
// add a client
const clientId = faker.random.uuid();
await addClient({name:"client name",description:"client description",id:clientId})
// call get clients and check the result
const res = await exec(token_admin);
expect(res.body.data.clients.length).toBe(1);
expect(res.body.data.clients[0]).toHaveProperty('name','client name');
expect(res.body.data.clients[0]).toHaveProperty('description','client description');
expect(res.body.data.clients[0]).toHaveProperty('id',clientId);
});
})
describe('Get client by ID', () => {
const exec = (token,clientId) => {
return request(app.callback())
.get('/clients/' + clientId)
.set('auth',token)
}
it('should return a 200 when super admin provide the action', async () => {
const clientId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,clientId);
expect(res.status).toBe(200);
});
it('should return a 404 when the client does not exist', async () => {
const nonExistingClientId = faker.random.uuid();
const res = await exec(token_admin,nonExistingClientId);
expect(res.status).toBe(404);
});
})
describe('Update client', () => {
const exec = (token,clientId,client) => {
return request(app.callback())
.patch('/clients/' + clientId)
.set('auth',token)
.send(client);
}
const clientModified = {
name:"name modified",
description:"description modified",
app_user_licenses: 15
}
it('should return a 200 when super admin provide the action', async () => {
const clientId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,clientId,clientModified);
expect(res.status).toBe(200);
// check if the client id modified
let clients = await getAllClients();
expect(clients.length).toBe(1);
expect(clients[0]).toHaveProperty('name',clientModified.name);
expect(clients[0]).toHaveProperty('description',clientModified.description);
expect(clients[0]).toHaveProperty('app_user_licenses',clientModified.app_user_licenses);
});
it('should return a 404 when the client does not exist', async () => {
const nonExistingClientId = faker.random.uuid();
const res = await exec(token_admin,nonExistingClientId,clientModified);
expect(res.status).toBe(404);
});
})
describe('Create client', () => {
const exec = (token,client) => {
return request(app.callback())
.post('/clients')
.set('auth',token)
.send(client);
}
it('should return a 200 when super admin does the action', async () => {
const res = await exec(token_admin,{name:"clientA",description:"description for clientA"});
expect(res.status).toBe(200);
});
it('list of clients should be appended when a new client is created', async () => {
let clients = await getAllClients();
expect(clients.length).toBe(0);
const res = await exec(token_admin,{name:"clientA",description:"description for clientA"});
expect(res.status).toBe(200);
clients = await getAllClients();
expect(clients.length).toBe(1);
expect(clients[0]).toHaveProperty('name','clientA');
expect(clients[0]).toHaveProperty('description','description for clientA');
});
});
describe('Delete client', () => {
const exec = (token,clientId) => {
return request(app.callback())
.delete('/clients/'+ clientId)
.set('auth',token);
}
it('should return a 200 when super admin does the action', async () => {
const clientId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,clientId);
expect(res.status).toBe(200);
});
it('should return a 404 when trying to delete a non-existing id', async () => {
const clientId = faker.random.uuid();
const nonExistingId = faker.random.uuid();
await addClient({id:clientId})
const res = await exec(token_admin,nonExistingId);
expect(res.status).toBe(404);
});
it('the client deleted should be removed from the list of clients', async () => {
const clientIdToDelete = faker.random.uuid();
const clientIdToRemain = faker.random.uuid();
await addClient({id:clientIdToRemain})
await addClient({id:clientIdToDelete})
let clients = await getAllClients();
expect(clients.length).toBe(2);
await exec(token_admin,clientIdToDelete);
clients = await getAllClients();
expect(clients.length).toBe(1);
expect(clients[0]).toHaveProperty('id',clientIdToRemain);
});
});
})
jest command: jest --coverage --forceExit --runInBand --collectCoverageFrom=src/**/*ts
I found the problem: I had a problem on the "deleteAllCollection" function, I forgot to put an "await".
Here is the correction for this function:
export async function deleteAllCollections() {
const collections = ["clients", "web_users","clients_web_users","clients_app_users","app_users"];
for (const collection of collections) {
await deleteCollection(collection);
};
}
Related
I wanted to write a method where onClick the google sign in starts and after successful sign in it makes a post request to my API.But the weird problem is 30% of the times the sign in data doesnt come to mongo db.I even called signout function in the catch block.Please help if someone notice any error!!
const Hero = () => {
const [user, setUser] = useState(null);
const [fetchUser, setFetchUser] = useState(null);
const handleGoogleSignIn = () => {
const googleProvider = new GoogleAuthProvider();
signInWithPopup(auth, googleProvider)
.then(async (result) => {
console.log(result);
try {
const { data } = await axios.post(
"https://myAPIherokuapp.com/api/v1/9c142e80023e07c3/registerUser",
{ name: result.user.displayName, email: result.user.email }
);
console.log(data);
} catch (err) {
console.log(err);
signOut(auth)
}
})
.catch((error) => {
console.log(error);
});
};
Maybe try async/await at the handleGoogleSignIn level? e.g.
const handleGoogleSignIn = async () => {
const googleProvider = await new GoogleAuthProvider();
const userResult = await signInWithPopup(auth, googleProvier);
await axios.post('url', userResult);
...
}
I think that should help?
I am really struggling to understand unit testing within a Serverless Application. So I obviously have my handler, and I have a single Lambda function
const responses = require('../utils/jsonResponse');
const someConnector = require('../services/connectToService/connectToService');
module.exports = async (event) => {
const connectionParams = {
//some env variables
};
try {
const token = await someConnector.connectToService(connectionParams);
return responses.status(token, 200);
} catch (e) {
return responses.status(
`Issue connecting to service - ${e.message}`,
500,
);
}
};
So this Lambda function is pretty straight forward, gets some environment variables, and awaits a response from a service. It then returns the response.
So I have already done integration tests for this which is fine, but now I wanted to do a Unit test. I wanted to test this function in isolation, so essentially I want to mock connectToService to return my own responses.
So I came up with the following
require('dotenv').config();
const { expect } = require('chai');
const sinon = require('sinon');
let sandbox = require("sinon").createSandbox();
const LambdaTester = require('lambda-tester');
const handler = require('../../../handler');
const msConnector = require('../../../services/connectToService/connectToService');
describe('Testing handler', async (done) => {
describe('endpoint someEndpoint returns 200', () => {
it('Should resolve with 200', async () => {
before(() => {
sandbox = sinon.createSandbox();
sandbox.stub(msConnector, 'connectToService').resolves('some-token');
});
afterEach(() => {
sandbox.restore();
});
await LambdaTester(handler.someEndpoint)
.expectResult((result) => {
console.log(result);
expect(result.statusCode).to.equal(200);
});
});
});
done();
});
msConnector is the filename of the service, connectToService is the function name. What I want to do is not invoke this function, but return some-token when my Lambda calls it.
However, I have the console.log, and what I get from that is the real token, not some-token.
This tells me that the mocked function is really being called and executed and returning the real value.
So how can I mock this to make sure it returns some-token?
Thanks
Service function
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports = {
connectToService,
};
The function connectToService may be not same copy between you mocked and called.
Because you overwrote a new object by module.exports = .... This causes you probably get different object for each require.
Try to do the below approach sharing the same object for all require.
const { DOMParser } = require('#xmldom/xmldom');
const axios = require('axios');
const { loginRequest } = require('./xml/login');
const connectToService = async (connectionParams) => {
//this injects config details into XML
const xmlRequest = loginRequest(
connectionParams.username,
connectionParams.password,
connectionParams.url,
);
const config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': xmlRequest.length,
},
};
const token = await axios
.post(connectionParams.msHost, xmlRequest, config)
.then((res) => {
const dom = new DOMParser().parseFromString(res.data, 'text/xml');
if (
dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0)
) {
return dom.documentElement
.getElementsByTagName('wsse:secToken')
.item(0).firstChild.nodeValue;
}
throw new Error('Invalid Username/Password');
})
.catch((err) => {
throw new Error(`Error making connection - ${err.message}`);
});
return token;
};
module.exports.connectToService = connectToService;
I have 2 files index.js and models.js
my index.js file requires the model.js file
const models = require("./modules/models.js");
app.post('/message', (req, res) => {
const _lead = models.createLead(req.body)
_lead.then(l => {
console.log("resulted in lead", JSON.stringify(l))
// GETTING UNDEFINED HERE
})
// const _message = await models.createMessage(req.body.message)
res.send();
});
My models.js file has a functions that contain Promises
const createLead = async function (payload) {
// Find lead
let result
new Promise((resolve, reject) => {
findLeadById(payload.lead.rid).then(async _lead => {
// If a lead has not been found
if (!_lead) {
if(payload.companyRid != payload.lead.rid){
await db.collection('leads').add(payload.lead).then((l) => {
result = payload.lead
result['id'] = l.id
})
}
} else {
result = _lead
}
}).then(() => {
console.log(" result of create lead ",JSON.stringify(result))
resolve(result)
}).catch(error => {
error.log("reject", error)
reject(error)
})
})
}
const findLeadById = async function (lead) {
new Promise(async (resolve, reject) => {
const ref = db.collection('leads');
console.log("finding lead", JSON.stringify(lead))
await ref.where("rid", "==", lead).get().then((s) => {
let obj = null
if(s){
s.forEach(doc => {
console.log("found the lead in loop", JSON.stringify(doc.data()))
obj = doc.data()
obj['id'] = doc.id
})
}
resolve(obj)
}).catch(error => {
error.log("reject", error)
reject(error)
})
})
}
exports.createLead = createLead;
exports.findLeadById = findLeadById;
The model.js returns an object as expected
console.log(" result of create lead ",JSON.stringify(result))
However, I'm expecting to see that same object in the index.js in the then block but I'm getting undefined.
console.log("resulted in lead", JSON.stringify(l))
// GETTING UNDEFINED HERE
The following is not waiting for the associated functions to finish
const _lead = models.createLead(req.body)
I have also tried adding async and await which also didn't work
const _lead = await models.createLead(req.body).then(res => {console.log("result", res) // Still undefined
})
I think you can use resolve for any success response, and reject for error response. Ex:
await db.collection('leads').add(payload.lead).then((l) => {
let result = payload.lead
result['id'] = l.id
resolve(result)
})
Am writing unit test case for my code, as am calling another lambda function inside my lambda am not sure how to mock the inner lambda value, so because of this my test case is getting timed out. Attaching my code below
Test case file
"use strict";
const sinon = require("sinon");
const AWS = require("aws-sdk");
const expect = require("chai").expect;
const models = require("common-lib").models;
const { Organization } = models;
const DATA_CONSTANTS = require("./data/deleteOrganization");
const wrapper = require("../../admin/deleteOrganization");
const sandbox = sinon.createSandbox();
describe("Start Test updateOrganization", () => {
beforeEach(() => {
sandbox.stub(Organization, "update").resolves([1]);
});
afterEach(async () => {
sandbox.restore();
});
it("Test 03: Test to check success returned by handler", async () => {
const mLambda = {
invoke: sinon.stub().returnsThis(),
promise: sinon.stub(),
};
const response = await wrapper.handler(
DATA_CONSTANTS.API_REQUEST_OBJECT_FOR_200
);
console.log({ response });
expect(response.statusCode).to.be.equal(200);
const body = JSON.parse(response.body);
expect(body.message).to.be.equal("Updated successfully");
});
});
Code function
exports.handler = asyncHandler(async (event) => {
InitLambda("userService-deleteOrganization", event);
const { id } = event.pathParameters;
if (isEmpty(id)) {
return badRequest({
message: userMessages[1021],
});
}
try {
const orgrepo = getRepo(Organization);
const [rowsUpdated] = await orgrepo.update(
{ isDeleted: true },
{ org_id: id }
);
if (!rowsUpdated) {
return notFound({
message: userMessages[1022],
});
}
const lambda = new AWS.Lambda({
region: process.env.region,
});
await lambda
.invoke({
FunctionName:
"user-service-" + process.env.stage + "-deleteOrganizationDetail",
InvocationType: "Event",
Payload: JSON.stringify({
pathParameters: { id },
headers: event.headers,
}),
})
.promise();
return success({
message: userMessages[1023],
});
} catch (err) {
log.error(err);
return failure({
error: err,
message: err.message,
});
}
});
It seems that you are not properly stubbing the AWS.Lambda object.
try this,
const sinon = require("sinon");
const AWS = require("aws-sdk");
const expect = require("chai").expect;
const models = require("common-lib").models;
const { Organization } = models;
const DATA_CONSTANTS = require("./data/deleteOrganization");
const wrapper = require("../../admin/deleteOrganization");
const sandbox = sinon.createSandbox();
describe("Start Test updateOrganization", () => {
beforeEach(() => {
sandbox.stub(Organization, "update").resolves([1]);
});
afterEach(async () => {
sandbox.restore();
});
it("Test 03: Test to check success returned by handler", async () => {
const mLambda = { invoke: sinon.stub().returnsThis(), promise: sinon.stub() };
// you missed the below line
sinon.stub(AWS, 'Lambda').callsFake(() => mLambda);
const response = await wrapper.handler(
DATA_CONSTANTS.API_REQUEST_OBJECT_FOR_200
);
console.log({ response });
expect(response.statusCode).to.be.equal(200);
const body = JSON.parse(response.body);
expect(body.message).to.be.equal("Updated successfully");
sinon.assert.calledOnce(AWS.Lambda);
sinon.assert.calledWith(mLambda.invoke, {});
sinon.assert.calledOnce(mLambda.promise);
});
});
I can see that,
You are writing entire logic inside your handler function. This makes it less testable.
To overcome this you can write your code in such a way that is divided into small functions, which are easy to mock in test case files or testable independently. Handler function should only make call to those functions and return the result to the caller.
for Eg.
Lambda Handler:
exports.lambdaHandler = async (event) => {
// do some init work here
const lambdaInvokeResponse = await exports.invokeLambda(params);
}
exports.invokeLambda = async (params) {
const response = await lambda.invoke(params).promise();
return response;
}
test cases:
it('My Test Case - Success', async () => {
const result = await app.lambdaHandler(event);
const invikeLambdaResponse = {
// some dummy response
};
sinon.replace(app, 'invokeLambda', sinon.fake.returns(invikeLambdaResponse ));
});
This is now mocking the only lambda invoke part.
You can mock all the external calls like this (dynamodb, invoke, sns, etc.)
You can set spy and check if the called method is called as per desired arguments
There are 2 mocha test files:
Creates a server and pings it using chai just to check if it's
working
Creates a server and tests user insertion into database (sequelize postgres)
Both of these servers initialize a database connection.
When ran independently both of them pass, when ran together the second one fails with the following error:
Error ConnectionManager.getConnection was called after the connection manager was closed
Looking at the console, connection with the database is established 2 times for each test, but still acts as a single pool.
# db/index.js
global.TABLE_USERS = 'users';
const Promise = require('bluebird');
const Sequelize = require('sequelize');
const config = require('./../config');
const User = require('./User');
/**
* #return {Promise}
*/
const connect = () => {
return new Promise((resolve, reject) => {
let sequelize = new Sequelize(config.postgres.database, config.postgres.user, config.postgres.password, {
host: config.postgres.host,
dialect: 'postgres',
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
define: {
underscored: false,
freezeTableName: false,
charset: 'utf8',
dialectOptions: {
collate: 'utf8_general_ci'
}
},
});
let user = User(sequelize);
sequelize
.authenticate()
.then(() => {
resolve({
User: user,
sequelize: sequelize
})
})
.catch(err => {
console.error('Couldn\'t authenticate');
reject(err)
})
});
};
module.exports.connect = connect;
Main server module:
const express = require('express');
const bodyParser = require('body-parser');
global.Promise = require('bluebird');
let routing = require('./routing');
const config = require('./config');
const middleware = require('./middleware');
let database = require('./db');
let Repositories = require('./repositories');
let Services = require('./services');
let Controllers = require('./controllers');
const Promise = require('bluebird');
/**
* #property {http.Server} this.app
*/
class Server {
constructor() {
this.app = express();
}
/**
* #param {Function} beforeHook
*
*/
init(beforeHook = null) {
return this._initDatabaseConnection()
.then(() => {
this._initContainer(beforeHook);
this._initRoutes();
return this._initServer()
});
}
/**
*
* #param {Function} beforeHook
* #private
*/
_initContainer(beforeHook) {
this.container = {};
// Modify for testing before starting
if (typeof beforeHook === 'function') beforeHook(this);
this.container = Repositories(this.database);
this.container = Services(this.container);
this.controllers = Controllers(this.container);
}
/**
*
* #private
*/
_initRoutes() {
this.app.use(bodyParser.json());
middleware.handleCors(this.app);
this.app.use(routing({...this.controllers, ...this.services}));
middleware.handleErrors(this.app);
}
/**
*
* #private
*
* #return {Promise}
*/
_initServer() {
return new Promise((resolve, reject) => {
this.server = this.app.listen(config.app.port, () => {
console.log(`Server started listening in ${config.app.env} on port ${config.app.port}`);
resolve(this)
});
});
}
/**
*
* #return {Promise}
* #private
*/
_initDatabaseConnection() {
return database.connect()
.then(connection => {
this.database = connection;
console.log('Connected to the database');
return Promise.resolve()
})
}
/**
* #return {Promise}
*/
close() {
this.server.close();
return this.database.sequelize.close();
}
}
module.exports = Server;
First test case
const assert = require('assert');
const chai = require('chai'),
expect = chai.expect,
chaiHttp = require('chai-http');
chai.use(chaiHttp);
const Server = require('../../src/Server');
describe('Server app test', () => {
let server;
before(async () => {
server = await (new Server()).init();
});
after(async () => {
await server.close();
});
it('should say respond it\'s name', async () => {
let pingServer = () => {
return new Promise((resolve, reject) => {
chai.request(server.server)
.get('/')
.end((err, res) => {
expect(err).to.be.null;
expect(res).to.have.status(200);
resolve(res.body)
});
});
};
let res = await pingServer();
assert.equal(res.msg, 'API server');
});
});
Second test case, UserControllerTest
const assert = require('assert');
const chai = require('chai'),
expect = chai.expect,
chaiHttp = require('chai-http');
chai.use(chaiHttp);
const sinon = require('sinon');
const Promise = require('bluebird');
const Response = require('./../../src/lib/RequestHelper');
const UserValidation = require('./../../src/validation/UserValidation');
const Server = require('./../../src/Server');
const ReCaptchaService = require('./../../src/services/ReCaptchaService');
const ValidationError = require('./../../src/errors/ValidationError');
describe('/users/signup', () => {
describe('valid reCaptcha scenario', () => {
let server, reCaptchaServiceStub;
before(async () => {
reCaptchaServiceStub = sinon.stub(ReCaptchaService.prototype, 'authenticate').returns(true);
function setReCaptchaServiceStub(server) {
server.services = {ReCaptchaService: new reCaptchaServiceStub()};
}
server = await (new Server()).init(setReCaptchaServiceStub);
});
after(async () => {
reCaptchaServiceStub.restore();
await server.database.User.destroy({where: {}});
await server.close();
});
beforeEach(async () => {
await server.database.User.destroy({where: {}});
});
it('should allow user to register', async () => {
let data = {email: 'myemail#gmail.com', password: '1234'};
data[UserValidation.CAPTCHA_RESPONSE] = 'captcha_token';
let signUp = (data) => {
return new Promise((resolve, reject) => {
chai.request(server.server)
.post('/users/signup')
.send(data)
.end((err, res) => {
console.log(res.body)
expect(err).to.be.null;
expect(res).to.have.status(Response.STATUS_OK);
resolve(res.body)
});
});
};
let res = await signUp(data);
expect(res.token).to.be.a('string');
});
});
describe('invalid reCaptcha scenario', () => {
let server, reCaptchaServiceStub;
before(async () => {
reCaptchaServiceStub = sinon.stub(ReCaptchaService.prototype, 'authenticate')
.onCall()
.throws(new ValidationError('some err'));
function setReCaptchaServiceStub(server) {
server.container.ReCaptchaService = new reCaptchaServiceStub()
}
server = await (new Server()).init(setReCaptchaServiceStub);
});
after(async () => {
reCaptchaServiceStub.restore();
await server.close();
});
beforeEach(async () => {
await server.database.User.destroy({where: {}});
});
it('should send a bad request on invalid reCaptcha', async () => {
let data = {email: 'myemail#gmail.com', password: '1234'};
data[UserValidation.CAPTCHA_RESPONSE] = 'random_token';
let signUp = (data) => {
return new Promise((resolve, reject) => {
chai.request(server.server)
.post('/users/signup')
.send(data)
.end((err, res) => {
expect(err).to.not.be.null;
expect(res).to.have.status(Response.STATUS_BAD_REQUEST);
resolve(res.body);
});
});
};
let res = await signUp(data);
expect(res.err).to.equal(UserValidation.ERR_INVALID_RECAPTCHA);
});
});
});
After doing more research into this, this is the following behaviour that caused the issue.
When mocha is ran to test the files recursively it is ran as a single process, this causes conflicts when closing the connection with sequelize.
To avoid this issue you should NOT close the connection with sequelize, but instead set an extra option with mocha --exit which terminates any additional cycles in the event loop after tests complete, thus closing the sequelize connection by itself.