Jest mock mongoose.startSession() throws error - mongodb

i'm implemented transaction in the post method. it was work fine. But now I have to update unit test case for that method. I tried to mock startSession() and startTransaction() to check toHaveBeenCalled.But while running test case i got like MongooseError: Connection 0 was disconnected when calling startSession``. I am new to that so i don't know how to mock that?.
Method:
static post = (funcCall: Promise<Document>) => async (response: Response, fields?: string[]) => {
const session = await startSession();
session.startTransaction();
try {
const dbResponse = await funcCall; // model.save(request.body)
// commit the changes if everything was successful
await session.commitTransaction();
success(pick(dbResponse, fields ? fields : ['_id']), 201)(response);
} catch (error) {
// this will rollback any changes made in the database
await session.abortTransaction();
throwError(error);
} finally {
// ending the session
session.endSession();
}
};
My Test case:
it('should perform post when valid parameters is passed.', async () => {
// Preparing
const mockSaveReturn = {
_id: objectID,
test_name: 'sample',
};
jest.spyOn(mongoose, 'startSession')
const spySave = jest.spyOn(modelPrototype.prototype, 'save').mockReturnValueOnce(mockSaveReturn);
const document = new modelPrototype(mockSaveReturn);
// Executing
await post(document.save())(response as Response);
expect(response.send).toHaveBeenCalledWith({ _id: '54759eb3c090d83494e2d804' });
expect(spySave).toHaveBeenCalled();
// Cleaning
spySave.mockClear();
});

Related

Error [ERR_HTTP_HEADERS_SENT] : Can't figure out the multipe requests

I have this error : Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client. From my understanding, the problem is that I am trying to send more than one response to the same http request. My instinct tell me that it’s this part that messes up :
catch (err) {
res.status(400).json(err);
}
Because if no user/password found in the DB, we already send status(400). Am I right ? More importantly (and that’s what drives me crazy), I am following a YT tuto and his code is exactly like mine, yet his seems to be working without any problem.
My code :
const router = require("express").Router();
const User = require("../models/Users");
const bcrypt = require("bcrypt");
//LOGIN
router.post("/login", async (req, res) => {
try {
const user = await User.findOne({ username: req.body.username });
!user && res.status(400).json("Wrong credentials!");
const validated = await bcrypt.compare(req.body.password, user.password);
!validated && res.status(400).json("Wrong credentiaaaals!");
const { password, ...others } = user._doc;
res.status(200).json(others);
} catch (err) {
res.status(500).json(err);
}
});
module.exports = router;
His code :
//LOGIN
router.post("/login", async (req, res) => {
try {
const user = await User.findOne({ username: req.body.username });
!user && res.status(400).json("Wrong credentials!");
const validated = await bcrypt.compare(req.body.password, user.password);
!validated && res.status(400).json("Wrong credentials!");
const { password, ...others } = user._doc;
res.status(200).json(others);
} catch (err) {
res.status(500).json(err);
}
});
module.exports = router;
Am I doing something wrong ? Is my reflexion bad ? Thanks !
You are right, your code is trying to send data to the client multiple times. The issue is that after the call .json("Wrong credentials!") completed, the write stream to the client will be closed, and you will not be able to send any other data to the client. The framework knows to detect it and show you the bug.
In your code, after the method .json("Wrong credentials!") finishes own execution, your program will continue and will try to execute the next lines...
You just need to add return, so the program will exit the current flow after it sends the response to the client.
const router = require("express").Router();
const User = require("../models/Users");
const bcrypt = require("bcrypt");
//LOGIN
router.post("/login", async (req, res) => {
try {
const user = await User.findOne({ username: req.body.username });
if (!user) {
return res.status(400).json("Wrong credentials!"); // without return the code will continue to execute next lines
}
const validated = await bcrypt.compare(req.body.password, user.password);
if (!validated) {
return res.status(400).json("Wrong credentiaaaals!"); // without return the code will continue to execute next lines
}
const { password, ...others } = user._doc;
res.status(200).json(others); // return is not necessary, because there is no cod which will be executed after we back from the json method
} catch (err) {
res.status(500).json(err); // return is not necessary, because there is no cod which will be executed after we back from the json method
}
});
module.exports = router;

MongoDB transaction with #NestJs/mongoose not working

I really need your help. My MongoDB transaction with #NestJs/mongoose not working...When My stripe payment fails rollback is not working... Still, my order collection saved the data...How can I fix this issue..?
async create(orderData: CreateOrderServiceDto): Promise<any> {
const session = await this.connection.startSession();
session.startTransaction();
try {
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save();
await this.stripeService.charge(
orderData.amount,
orderData.paymentMethodId,
orderData.stripeCustomerId,
);
await session.commitTransaction();
return order;
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
I had the same issue and i found that on github: Mongo DB Transactions With Mongoose & Nestjs
So I think, according this issue, you have to call the create method of your model, like that:
const order = await this.orderModel.create(orderData, { session });
as you can see, the Model.create method has an overload with SaveOptions as parameter:
create(docs: (AnyKeys<T> | AnyObject)[], options?: SaveOptions): Promise<HydratedDocument<T, TMethodsAndOverrides, TVirtuals>[]>;
it takes an optional SaveOptions parameter that can contain the session:
interface SaveOptions {
checkKeys?: boolean;
j?: boolean;
safe?: boolean | WriteConcern;
session?: ClientSession | null;
timestamps?: boolean;
validateBeforeSave?: boolean;
validateModifiedOnly?: boolean;
w?: number | string;
wtimeout?: number;
}
Please note that Model.save() can also take a SaveOptions parameter.
So you can also do as you did like that:
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save({ session });
A little further...
As i do many things that require a transaction, I came up with this helper to avoid many code duplication:
import { InternalServerErrorException } from "#nestjs/common"
import { Connection, ClientSession } from "mongoose"
export const mongooseTransactionHandler = async <T = any>(
method: (session: ClientSession) => Promise<T>,
onError: (error: any) => any,
connection: Connection, session?: ClientSession
): Promise<T> => {
const isSessionFurnished = session === undefined ? false : true
if (isSessionFurnished === false) {
session = await connection.startSession()
session.startTransaction()
}
let error
let result: T
try {
result = await method(session)
if (isSessionFurnished === false) {
await session.commitTransaction()
}
} catch (err) {
error = err
if (isSessionFurnished === false) {
await session.abortTransaction()
}
} finally {
if (isSessionFurnished === false) {
await session.endSession()
}
if (error) {
onError(error)
}
return result
}
}
Details
the optional parameter session is in case you are doing nested nested transaction.
that's why i check if the session is provided. If it is, it means we are in a nested transaction. So we'll let the main transaction commit, abort and end the session.
Example
for example: you delete a User model, and then the user's avatar which is a File model.
/** UserService **/
async deleteById(id: string): Promise<void> {
const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
const user = await this.userModel.findOneAndDelete(id, { session })
await this.fileService.deleteById(user.avatar._id.toString(), session)
}
const onError = (error: any) => {
throw error
}
await mongooseTransactionHandler<void>(
transactionHandlerMethod,
onError,
this.connection
)
}
/** FileService **/
async deleteById(id: string, session?: ClientSession): Promise<void> {
const transactionHandlerMethod = async (session: ClientSession): Promise<void> => {
await this.fileModel.findOneAndRemove(id, { session })
}
const onError = (error: any) => {
throw error
}
await mongooseTransactionHandler<void>(
transactionHandlerMethod,
onError,
this.connection,
session
)
}
So, in short:
You can use it like this:
async create(orderData: CreateOrderServiceDto): Promise<any> {
const transactionHandlerMethod = async (session: ClientSession): Promise<Order> => {
const createOrder = new this.orderModel(orderData);
const order = await createOrder.save({ session });
await this.stripeService.charge(
orderData.amount,
orderData.paymentMethodId,
orderData.stripeCustomerId,
);
return order
}
const onError = (error: any): void => {
throw error
}
const order = await mongooseTransactionHandler<Order>(
transactionHandlerMethod,
onError,
this.connection
)
return order
}
Hope it'll help.
EDIT
Do not abuse of the model.save({ session }) of the same model in nested transcations.
For some reasons it will throw an error the model is updated too many times.
To avoid that, prefer using model embeded methods that update and return a new instance of your model (model.findOneAndUpdate for example).

What is wrong with my findOne() implementation?

I have followed all basic tutorials in search to fix this. My implementation of collection.findOne({_id: ctx.params.id}) is just not working.
// import statements for version transparancy, they are not really used here
import { MongoClient } from "https://deno.land/x/mongo#v0.29.2/mod.ts";
import { Application, Router } from "https://deno.land/x/oak#v10.4.0/mod.ts";
// ...
const recipes = db.collection<Recipe>("recipes");
// ...
router.get("/rec/:id", async (ctx) => {
const id = ctx.params.id;
const recipe = await recipes.findOne({ _id: { $oid: id } });
// const recipe = await recipes.findOne({_id: id});
if (recipe) {
ctx.response.status = 200;
ctx.response.body = recipe;
} else {
ctx.response.status = 404;
ctx.response.body = { message: "No recipe found" };
}
});
Is there a current bug that I am not aware of?
Once I call that endpoint with http://localhost:5000/rec/622b6be81089abbc4b4144a1 (and yes, that ID is in my database), I get an internal server error (500) with the following message:
[uncaught application error]: Error - MongoError: {"ok":0,"errmsg":"unknown operator: $oid","code":2,"codeName":"BadValue"}
request: {
url: "http://localhost:5000/rec/622b6be81089abbc4b4144a1",
method: "GET",
hasBody: false
}
response: { status: 404, type: undefined, hasBody: false, writable: true }
at WireProtocol.commandSingle (https://deno.land/x/mongo#v0.29.2/src/protocol/protocol.ts:44:13)
at async FindCursor.executor (https://deno.land/x/mongo#v0.29.2/src/collection/commands/find.ts:17:24)
at async FindCursor.execute (https://deno.land/x/mongo#v0.29.2/src/protocol/cursor.ts:34:21)
at async FindCursor.next (https://deno.land/x/mongo#v0.29.2/src/protocol/cursor.ts:48:7)
at async file:///home/andy/dev/deno/denodb/routes.ts:42:20
at async dispatch (https://deno.land/x/oak#v10.4.0/middleware.ts:41:7)
at async dispatch (https://deno.land/x/oak#v10.4.0/middleware.ts:41:7)
at async dispatch (https://deno.land/x/oak#v10.4.0/middleware.ts:41:7)
at async Application.#handleRequest (https://deno.land/x/oak#v10.4.0/application.ts:376:9)
Downgrading the version did not help either.

Why are my tests occasionally passing and occasionally failing? (Jest, Mongoose, MongoDB)

I have a setup file for my tests that looks like this:
const mongoose = require('mongoose');
mongoose.set('useCreateIndex', true);
mongoose.promise = global.Promise;
async function removeAllCollections() {
const collections = Object.keys(mongoose.connection.collections);
for (const collectionName of collections) {
const collection = mongoose.connection.collections[collectionName];
await collection.deleteMany();
}
}
async function dropAllCollections() {
const collections = Object.keys(mongoose.connection.collections);
for (const collectionName of collections) {
const collection = mongoose.connection.collections[collectionName];
try {
await collection.drop();
} catch (error) {
// Sometimes this error happens, but you can safely ignore it
if (error.message === 'ns not found') return;
// This error occurs when you use it.todo. You can
// safely ignore this error too
if (error.message.includes('a background operation is currently running'))
return;
console.log(error.message);
}
}
}
export default function setupDB(databaseName) {
// Connect to Mongoose
beforeAll(async () => {
const url = `mongodb://127.0.0.1/${databaseName}`;
await mongoose.connect(
url,
{
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
},
err => {
if (err) {
console.error(err);
process.exit(1);
}
}
);
});
// Cleans up database between each test
afterEach(async () => {
await removeAllCollections();
});
// Disconnect Mongoose
afterAll(async () => {
await dropAllCollections();
await mongoose.connection.close();
});
}
I am then writing tests like this:
import User from 'db/models/User';
import setupDB from 'utils/setupDbForTesting';
setupDB('mongoose_bcrypt_test');
it('correctly hashes and salts passwords', async done => {
// create a user a new user
const newUser = new User({
username: 'jmar777',
password: 'Password123'
});
await newUser.save(function (err) {
if (err) {
console.log(err);
}
});
const user = await User.findOne({ username: 'jmar777' });
user.comparePassword('Password123', function (err, isMatch) {
if (err) throw err;
expect(isMatch).toBeTruthy();
});
user.comparePassword('123Password', function (err, isMatch) {
if (err) throw err;
expect(isMatch).toBeFalsy();
});
done();
});
However, every other time I run these tests, they pass (or fail) so for every time T that the tests pass, T + 1 they will fail. My question is - why?
The tests fail because user (in the callback for User.findOne) returns null, even though the user has been saved.
I think the issue lies in the tearing down of the database, but I really can't see any problems. Any help would be appreciated, thanks.

Integration Testing Nodejs/Express/Mongoose with Jest/Supertest One Mongoose Model Saves, One Mongoose Model Doesn't

I use Postman and the ReactJS UI to call this registration execution and it works as I expect. Ironically, the Jest and Supertest integration tests do not produce expected results. When integration testing, the Profile is created and the User is not.
The architecture is pretty simple. MongoDB in a Docker container, and Node using nodemon in VSCode.
I have to be doing something wrong, I just can't spot what it is.
// The Integration Test __test__/users/../user.test.js
const app = require('../../app');
const uuidv4 = require('uuid/v4');
const User = require('../../src/models/User');
const Profile = require('../../src/models/Profile');
const bcrypt = require('bcryptjs');
const mongoose = require('mongoose');
const request = require("supertest");
const {
MONGO_URI,
TEST_DB_NAME
} = process.env;
let DB_URI = MONGO_URI + TEST_DB_NAME;
let NAME = TEST_DB_NAME;
mongoose.connect(DB_URI, {
useNewUrlParser: true,
useCreateIndex: true,
dbName: NAME
});
describe('User Integration Test', () => {
// make sure app is imported without issues
it('Has App Defined', () => {
expect(app).toBeDefined();
});
let server;
beforeAll(async () => {
// Clear Test Data
await User.deleteMany({});
await Profile.deleteMany({});
server = await app.listen(3001);
});
afterAll(async (done) => {
// Clear Test Data
await User.deleteMany({});
await Profile.deleteMany({});
// Close server
await server.close(done);
});
describe('User route tests', () => {
it('Can Register a User', async () => {
const body = {
"username": "User21",
"email": "user21#user.com",
"password": "123456",
"avatar": "image.jpg"
}
await request(server)
.post('/api/v1/users')
.send(body)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.expect(200)
});
});
// THE EXPRESS ROUTE in api/v1/users.js
const express = require('express');
const auth = require('../../middleware/auth');
const router = express.Router();
const { UserService } = require('../../services');
const {
check,
validationResult
} = require('express-validator/check');
// #route POST api/users
// #desc Register User
// #access Public
// #return status message
router.post('/', [
check('email', 'Please provide a valid email address').isEmail(),
check('password', 'Please enter a password with 6 or more characters').isLength({ min: 6 }),
check('username', 'Username is Required.').not().isEmpty()
], async (req, res, next) => {
try {
//--Validate
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const message = await UserService.register(req.body);
return res.status(200).json(message)
} catch (err) {
next(err);
}
});
// THE register METHOD found in ../../services/UserService.js
const register = async (data) => {
try {
// Destructure the data
const {
username,
email,
password,
avatar
} = data;
// remove spaces from username and lcase it
let user_name = username.replace(/\s/g, '').toLowerCase();
// Check if the username or email already exists
await doesUserExist(user_name, email);
// Create a new user
const token = uuidv4();
user = new User({
email: email.toLowerCase(),
username: user_name,
avatar: avatar,
verifyEmailToken: token
});
// encrypt the password
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
// Save the user
// (Works Unless Running Jest Integration Tests)
await user.save();
// Create and save an empty Profile for the new user
profile = new Profile();
profile.user = user;
// (Always Works)
await profile.save();
// Send verification email
await send(user, 'Verify Your Email', token, 'verify-email.html');
return { message: 'User was registered successfully.' };
} catch (err) {
throw err;
}
}
// Does user exist method found in ./UserService.js
const doesUserExist = async (username, email) => {
// Check if user exists by email
let message = await checkEmail(email);
if (!message.email_available) {
throw new Error('Email already exists');
}
// Check if user exists by username
message = await checkUserName(username.toLowerCase())
if (!message.username_available) {
throw new Error('Username already exists');
}
return false;
}
When I call this code via the UI, Postman, or curl both the User and Profile are created, as expected.
When I run the Integration Test, npm run test:integration or npm test,
Only the Profile is created.
my package.json scripts:
"test": "jest",
"test:integration": "jest --testPathPattern integration.test",
Finally, no errors are reported anywhere. User simply isn't created.
After a few hours of work and testing this issue I found that the afterAll() event was raise randomly. Sometimes after all the tests in the inner describe() ran and sometimes not. Of course, when afterAll() ran all Users where deleted from the data store.
If moved beforeAll() and AfterAll() to the inner describe(). This worked very well until I included other integration tests, like authorization, for example that also used the User table. I noticed that the test suites are not executed synchronously. As one test suit was blowing out the data in another test suite as the other test suite was executing.
I now set up a database per test suite. Clunky, wonky, hacky and wrong I know but I have to have move on. Does anyone know if you can control the synchronous and/or asynchronous behavior of Jest? Please don't suggest Mocha and/or Chai.
I ran into a similar issue, where there appeared to be an intermittent race condition between seeding a document and then retrieving it.
I fixed it by running jest with the --runInBand flag.