Why are my tests occasionally passing and occasionally failing? (Jest, Mongoose, MongoDB) - 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.

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;

Getting db.close is not a function in sample JEST

I'm trying to setup testing my node with mongo app using Jest. I've set it up and copied their sample verbatim, which works fine, except when I call the db.close(); function. It gives me a TypeError: that db.close is not a function. This is directly out of their example:
import { MongoClient } from 'mongodb';
describe('insert', () => {
let connection;
let db;
beforeAll(async () => {
connection = await MongoClient.connect(global.__MONGO_URI__, {
useNewUrlParser: true,
});
db = await connection.db(global.__MONGO_DB_NAME__);
});
afterAll(async () => {
await connection.close();
await db.close();
});
it('should insert a doc into collection', async () => {
const users = db.collection('users');
const mockUser = {_id: 'some-user-id', name: 'John'};
await users.insertOne(mockUser);
const insertedUser = await users.findOne({_id: 'some-user-id'});
expect(insertedUser).toEqual(mockUser);
});
});

Unit and integration test of Express REST API and multer single file update middleware

Introduction
Hello everybody,
I'm pretty new to unit and integration testing. The current REST API I'm working on involves file uploads and file system. If you want me to explain what's API this is, I can explain it to you using few sentences. Imagine a system like Microsoft Word. There are only users and users have documents. Users' documents are only JSON files and they are able to upload JSON file to add a document. My API currently has 3 routes, 2 middlewares.
Routes:
auth.js (authorization route)
documents.js (document centered CRUD operations)
users.js
Middlewares:
auth.js (To check if there is valid JSON web token to continue)
uploadFile.js (To upload single file using multer)
I have been able to unit/integration test auth.js, users.js routes and auth.js middleware. These routes and middlewares were only involving small packages of data I/O, so they were pretty easy for me. But documents.js router and uploadFile.js middleware is pretty hard for me to overcome.
Let me share my problems.
Source codes
documents.js Router
.
.
.
router.post('/mine', [auth, uploadFile], async (req, res) => {
const user = await User.findById(req.user._id);
user.leftDiskSpace(function(err, leftSpace) {
if(err) {
return res.status(400).send(createError(err.message, 400));
} else {
if(leftSpace < 0) {
fs.access(req.file.path, (err) => {
if(err) {
res.status(403).send(createError('Your plan\'s disk space is exceeded.', 403));
} else {
fs.unlink(req.file.path, (err) => {
if(err) res.status(500).send('Silinmek istenen doküman diskten silinemedi.');
else res.status(403).send(createError('Your plan\'s disk space is exceeded.', 403));
});
}
});
} else {
let document = new Document({
filename: req.file.filename,
path: `/uploads/${req.user.username}/${req.file.filename}`,
size: req.file.size
});
document.save()
.then((savedDocument) => {
user.documents.push(savedDocument._id);
user.save()
.then(() => res.send(savedDocument));
});
}
}
});
});
.
.
.
uploadFile.js Middleware
const fs = require('fs');
const path = require('path');
const createError = require('./../helpers/createError');
const jsonFileFilter = require('./../helpers/jsonFileFilter');
const multer = require('multer');
const storage = multer.diskStorage({
destination: function(req, file, cb) {
console.log('file: ', file);
if(!req.user.username) return cb(new Error('Dokümanın yükleneceği klasör için isim belirtilmemiş.'), null);
let uploadDestination = path.join(process.cwd(), 'uploads', req.user.username);
fs.access(uploadDestination, (err) => {
if(err) {
// Directory with username doesn't exist in uploads folder, so create one
fs.mkdir(uploadDestination, (err) => {
if(err) cb(err, null);
cb(null, uploadDestination);
});
} else {
// Directory with username exists
cb(null, uploadDestination);
}
});
},
filename: function(req, file, cb) {
cb(null, `${file.originalname.replace('.json', '')}--${Date.now()}.json`);
}
});
module.exports = function(req, res, next) {
multer({ storage: storage, fileFilter: jsonFileFilter }).single('document')(req, res, function(err) {
if(req.fileValidationError) return res.status(400).send(createError(req.fileValidationError.message, 400));
else if(!req.file) return res.status(400).send(createError('Herhangi bir doküman seçilmedi.', 400));
else if(err instanceof multer.MulterError) return res.status(500).send(createError(err.message, 500));
else if(err) return res.status(500).send(createError(err, 500));
else next();
});
}
Questions
1. How can I test user.leftDiskSpace(function(err, leftSpace) { ... }); function which has a callback and contains some Node.js fs methods which also has callbacks?
I want to reach branches and statements user.leftDiskSpace() function containing. I thought of using mock functions to mock out the function but I don't know how to do so.
2. How to change multer disk storage's upload destination for a specified testing folder?
Currently my API uploads the test documents to development/production uploads disk storage destination. What is the best way to change upload destination for testing? I thought to use NODE_ENV global variable to check if the API is being tested or not and change destination in uploadFile.js middleware but I'm not sure if it's a good solution of this problem. What should I do?
Current documents.test.js file
const request = require('supertest');
const { Document } = require('../../../models/document');
const { User } = require('../../../models/user');
const mongoose = require('mongoose');
const path = require('path');
let server;
describe('/api/documents', () => {
beforeEach(() => { server = require('../../../bin/www'); });
afterEach(async () => {
server.close();
await User.deleteMany({});
await Document.deleteMany({});
});
.
.
.
describe('POST /mine', () => {
let user;
let token;
let file;
const exec = async () => {
return await request(server)
.post('/api/documents/mine')
.set('x-auth-token', token)
.attach('document', file);
}
beforeEach(async () => {
user = new User({
username: 'user',
password: '1234'
});
await user.save();
token = user.generateAuthToken();
file = path.join(process.cwd(), 'tests', 'integration', 'files', 'test.json');
});
it('should return 400 if no documents attached', async () => {
file = undefined;
const res = await exec();
expect(res.status).toBe(400);
});
it('should return 400 if a non-JSON document attached', async () => {
file = path.join(process.cwd(), 'tests', 'integration', 'files', 'test.png');
const res = await exec();
expect(res.status).toBe(400);
});
});
});

Firebase cloud functions not waiting for forEach to complete before jumping to the next then

Been trying to copy subcollections of a collection into another collection. The code below is aimed at that, but jumps from the first then and logs out "Done" without logging out anything before.
So the question is what is not correct here?
exports = module.exports = functions.https.onRequest(async (req, res) => {
let db = admin.firestore();
try {
await db.collection("users").get().then((query) => {
return query.forEach(async (doc) => {
console.log("Here"); //This doesn't print
const polCollection = await db.collection("users").doc(doc.id).collection("xyz").get();
if (polCollection.docs.length > 0) { //This checks if any subcollections
for (const x of polCollection.docs) { //This copies them into a doc in the copy collection
db.collection("CopyUsers")
.doc(doc.id)
.set({ x : x.data() }, { merge: true });
}
}
});
})
.then(() => {
console.log("Done"); //This is the only thing that prints in the console
res.end();
})
.catch((e) => {
console.log("e", e);
res.end();
});
} catch (error) {
console.log("error", error);
res.end();
}
});
After the suggestion below, it now looks as follows:
exports = module.exports = functions.runWith(runtimeOpts).https.onRequest(async (req, res) => {
const promises = [];
let count = 0;
let size = 0;
return await admin
.firestore()
.collection("testUsers")
.get()
.then((query) => {
console.log("query length:", query.size); //prints x of users
size = query.size;
query.forEach(async (doc) => {
const promise = async () => {
console.log("Here", doc.id); //This doesn't print
await admin
.firestore()
.collection("testUsers")
.doc(doc.id)
.collection("xyz")
.get()
.then(async (polCollection) => {
if (polCollection.docs.length > 0) {
for (const x of polCollection.docs) {
return await admin
.firestore()
.collection("testBackUpUsers")
.doc(doc.id)
.set(
{ xyz: x.data() },
{ merge: true }
);
}
} else {
return;
}
})
.catch((e) => console.log("error from then after get xyz", e));
};
count++;
return promises.push(promise);
});
return promises;
})
.then(async (promises) => {
if (size <= count) {
console.log("running return Promise.all(promises)", promises.length); //prints number of promises = users
return Promise.all(promises);
}
})
.catch((e) => console.log("err from the last catch", e));
});
Any thoughts?
Unfortunately the forEach iterator does not support async/await. Even if you write an await inside it will just go trough it without waiting on the execution.
I would recommend to use Promise.all. That would also execute the code in parallel and would finish faster.
If you would only change data you could also use a batch change but in your example you first get the data and then change it.
Here is an example how you could write your code:
exports = module.exports = functions.https.onRequest(async (req, res) => {
let db = admin.firestore();
const promises = [];
try {
const query = await db.collection("users").get();
query.forEach((doc) => {
console.log("doc", doc);
const promise = async () => {
console.log("Here", doc.id); //This doesn't print
const polCollection = await db
.collection("users")
.doc(doc.id)
.collection("xyz")
.get();
if (polCollection.docs.length > 0) {
//This checks if any subcollections
for (const x of polCollection.docs) {
//This copies them into a doc in the copy collection
await db
.collection("CopyUsers")
.doc(doc.id)
.set({ x: x.data() }, { merge: true });
}
}
};
promises.push(promise);
});
console.log("promises", promises);
await Promise.all(promises);
console.log("Done");
res.end();
} catch (error) {
console.log("error", error);
res.end();
}
});

MongoDB issue with saving value returned from findByID

I have an issue with function which update password. What I would like to have is a function which will update logged user data.
export const updateMe = async (req, res, next) => {
if (!req) {
res.status(400).end()
}
try {
const updatedDoc = await User.findById(req.user._id, function(err, doc) {
if (err) return next(err)
doc.password = req.body.password
doc.save()
})
.lean()
.exec()
res.status(200).json({ data: updatedDoc })
} catch (e) {
console.log(e)
res.status(400).end()
}
}
I have written middleware which will hash password before it will be saved.
userSchema.pre('save', function(next) {
if (!this.isModified('password')) {
return next()
}
bcrypt.hash(this.password, 8, (err, hash) => {
if (err) {
return next(err)
}
this.password = hash
next()
})
})
I do not know why error is always reciving with message "doc.save() is not a funcition"
You are mixing promise and await code, also doc.save() returns a promise so you need to await it.
( I assume you are already setting req.user._id in a middleware, and it is not null.)
So your method must be like this if async/await is used:
export const updateMe = async (req, res, next) => {
if (!req.body.password) {
return res.status(400).send("Password is required");
}
try {
let updatedDoc = await User.findById(req.user._id);
updatedDoc.password = req.body.password;
updatedDoc = await updatedDoc.save();
res.status(200).json({ data: updatedDoc });
} catch (e) {
console.log(e);
res.status(400);
}
};