I am testing an express route handler which makes use of mongoose.
My route handler code is the following.
// Require models.
var Comment = require('../models/commentModel');
var User = require('../models/userModel');
var Post = require('../models/postModel');
// Require mongoose and set bluebird to handle its promises.
var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
// Creates a comment.
exports.create_comment = function(req, res) {
var comment = new Comment();
return comment.save().then(function(createdComment) {
res.json(createdComment);
var promises = [
User.findById(userId).exec(),
Post.findById(postId).exec()
];
// Should resolve to [{ user: {/* */} }, { post: {/* */} }]
var promisedDocs = Promise.all(promises);
// Function provided to Array.map().
function pushAndSave(obj) {
// Extract the doc from { doc: {/* */} }
var doc = obj[Object.keys(obj)[0];
doc.comments.push(createdComment);
return doc.save();
}
// Return promise and process the docs returned by resolved promise.
return promisedDocs.then(function(results) {
results.map(pushAndSave);
});
})
.catch(function(err) {
res.json(err);
});
}
The logic I am trying to test is that when everything goes right, the calls to the appropriate functions are made. Basically, I am expecting the following:
comment.save(), User.findById().exec(), Post.findById().exec(), user.save(), and post.save() to be called.
To test this, I am using mocha, chai, sinon, sinon-mongoose, sinon-stub-promise, and node-mocks-http.
This is my test (I am obviating the setup).
it('Makes all the appropriate calls when everyting goes right', function() {
// Mock through sinon-mongoose
var userMock = sinon.mock(User);
var postMock = sinon.mock(Post);
userMock
.expects('findById')
.chain('exec')
.resolves({
user: {/**/}
});
postMock
.expects('findById')
.chain('exec')
.resolves({
post: {/**/}
});
// Stubbing calls to Model#save.
var saveComment = sinon.stub(Comment.prototype, 'save');
var saveUser = sinon.stub(User.prototype, 'save');
var savePost = sinon.stub(Post.prototype, 'save');
saveComment.returnsPromise().resolves({
comment: {/**/}
});
saveUser.returnsPromise().resolves({
user: {/**/}
});
savePost.returnsPromise().resolves({
post: {/**/}
});
// Mocking req and res with node-mocks-http
req = mockHttp.createRequest({
method: 'POST',
url: '/comments',
user: {/**/},
body: {/**/}
});
res = mockHttp.createResponse();
// Call the handler.
commentsController.create_comment(req, res);
expect(saveComment.called).to.equal(true); // Pass
userMock.verify(); // Pass
postMock.verify(); // Pass
expect(saveUser.called).to.equal(true); // Fail
expect(savePost.called).to.equal(true); // Fail
});
As you can see, the calls to user.save() and post.save() are not made. This might be a problem with my Promise.all() setup and subsequent handling or my test itself, but I am out of ideas.
Can you spot my error?
Thanks in advance, guys.
It took me longer to recreate the missing parts of your example, than to actually find the problem. Below you can find a fixed version of your test scenario.
user.js
var mongoose = require('mongoose');
module.exports = mongoose.model('User', {
name: String,
comments: Array
});
post.js
var mongoose = require('mongoose');
module.exports = mongoose.model('Post', {
name: String,
comments: Array
});
comment.js
var mongoose = require('mongoose');
module.exports = mongoose.model('Comment', {
text: String
});
controller.js
var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
var Comment = require('./comment');
var User = require('./user');
var Post = require('./post');
// Creates a comment.
exports.create_comment = function (req, res) {
var comment = new Comment();
const userId = req.user.id;
const postId = req.body.id;
return comment.save().then(function (createdComment) {
res.json(createdComment);
var promises = [
User.findById(userId).exec(),
Post.findById(postId).exec()
];
// Should resolve to [{ user: {/* */} }, { post: {/* */} }]
var promisedDocs = Promise.all(promises);
// Function provided to Array.map().
function pushAndSave (doc) {
doc.comments.push(createdComment);
return doc.save();
}
// Return promise and process the docs returned by resolved promise.
return promisedDocs.then(function (results) {
results.map(pushAndSave);
});
})
.catch(function (err) {
console.error('foo', err);
res.json(err);
});
};
test.js
'use strict';
const chai = require('chai');
const sinon = require('sinon');
const SinonChai = require('sinon-chai');
var sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);
require('sinon-mongoose');
chai.use(SinonChai);
const expect = chai.expect;
var mockHttp = require('node-mocks-http');
const commentsController = require('./controller');
var Comment = require('./comment');
var User = require('./user');
var Post = require('./post');
describe.only('Test', () => {
it('Makes all the appropriate calls when everyting goes right',
function (done) {
// Mock through sinon-mongoose
var userMock = sinon.mock(User);
var postMock = sinon.mock(Post);
userMock
.expects('findById')
.chain('exec')
.resolves(new User());
postMock
.expects('findById')
.chain('exec')
.resolves(new Post());
// Stubbing calls to Model#save.
var saveComment = sinon.stub(Comment.prototype, 'save');
var saveUser = sinon.stub(User.prototype, 'save');
var savePost = sinon.stub(Post.prototype, 'save');
saveComment.resolves({
comment: { /**/}
});
saveUser.resolves({
user: { /**/}
});
savePost.resolves({
post: { /**/}
});
// Mocking req and res with node-mocks-http
const req = mockHttp.createRequest({
method: 'POST',
url: '/comments',
user: {id: '123'},
body: {id: 'xxx'}
});
const res = mockHttp.createResponse();
// Call the handler.
commentsController.create_comment(req, res).then(() => {
expect(saveComment.called).to.equal(true); // Pass
userMock.verify(); // Pass
postMock.verify(); // Pass
expect(saveUser.called).to.equal(true); // Fail
expect(savePost.called).to.equal(true); // Fail
done();
});
});
});
Overall, the assertion logic was ok in theory. The problems that I found were the following:
Your userMock and postMock returned a plain empty object instead of a Mongoose model instance. Therefore, there was an exception thrown within the pushAndSave function that was never caught, since the .save() method was undefined. I simplified the pushAndSave function so that you can see what I'm talking about
You tested an async method in sync mode and this would definitely cause issues with the assertions. I've switched the test case into async and the assertions are executed as soon as the method completes
Related
I'm using axios <0.22 version so I can use cancelToken but I dont understand how I can use it
I tried but it doesnt work. Help me please. How cancel requests if they are calling same endpoint?
let req = {}
const authInterceptors = (cfg) => {
const config = cfg;
req = cfg;
config.headers.common['X-XSRF-TOKEN'] = antiForgeryToken;
if (req[config.url]) {
req[config.url].cancel('Automatic cancellation')
}
const axiosSource = axios.CancelToken.source()
req[config.url] = { cancel: axiosSource.cancel }
config.cancelToken = axiosSource.token
return config;
};
const errorInterceptors = (error) => {
return Promise.reject(error);
};
const httpClient = axios.create({
headers: { 'Cache-Control': 'no-cache' },
adapter: throttleAdapterEnhancer(<AxiosAdapter>axios.defaults.adapter, { threshold: 3 * 1000 }),
});
httpClient.interceptors.request.use(authInterceptors, errorInterceptors);
First off, just know CancelToken according to docs:
is deprecated since v0.22.0 and shouldn't be used in new projects
To use CancelToken, there's just 3 steps:
create a CancelToken
assign that token to a request
invoke the method on CancelToken
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
or
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
Docs: https://axios-http.com/docs/cancellation
I have a documents router which has router.post('/mine', [auth, uploadFile], async (req, res) => { ... }) route handler. The actual implementation of this route handler is below.
documents.js router
const createError = require('./../helpers/createError');
const auth = require('./../middlewares/auth');
const uploadFile = require('./../middlewares/uploadFile');
const express = require('express');
const router = express.Router();
router.post('/mine', [auth, uploadFile], async (req, res) => {
try {
let user = await User.findById(req.user._id);
let leftDiskSpace = await user.leftDiskSpace();
if(leftDiskSpace < 0) {
await accessAndRemoveFile(req.file.path);
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 = await document.save();
user.documents.push(document._id);
user = await user.save();
res.send(document);
}
} catch(ex) {
res.status(500).send(createError(ex.message, 500));
}
});
module.exports = router;
I'm currently writing integration tests using Jest and Supertest. My current documents.test.js test file is below:
documents.test.js test file
const request = require('supertest');
const { Document } = require('../../../models/document');
const { User } = require('../../../models/user');
const fs = require('fs');
const path = require('path');
let server;
describe('/api/documents', () => {
beforeEach(() => { server = require('../../../bin/www'); });
afterEach(async () => {
let pathToTestFolder = path.join(process.cwd(), config.get('diskStorage.destination'), 'user');
// Remove test uploads folder for next tests
await fs.promises.access(pathToTestFolder)
.then(() => fs.promises.rm(pathToTestFolder, { recursive: true }))
.catch((err) => { return; });
// Remove all users and documents written in test database
await User.deleteMany({});
await Document.deleteMany({});
server.close();
});
describe('POST /mine', () => {
it('should call user.leftDiskSpace method once', async () => {
let user = new User({
username: 'user',
password: '1234'
});
user = await user.save();
let token = user.generateAuthToken();
let file = path.join(process.cwd(), 'tests', 'integration', 'files', 'test.json');
let documentsRouter = require('../../../routes/documents');
let errorToThrow = new Error('An error occured...');
user.leftDiskSpace = jest.fn().mockRejectedValue(errorToThrow);
let mockReq = { user: user };
let mockRes = {};
documentsRouter.post = jest.fn();
documentsRouter.post.mockImplementation((path, callback) => {
if(path === '/mine') {
console.warn('called');
callback(mockReq, mockRes);
}
});
const res = await request(server)
.post('/api/documents/mine')
.set('x-auth-token', token)
.attach('document', file);
expect(documentsRouter.post).toHaveBeenCalled();
expect(user.leftDiskSpace).toHaveBeenCalled();
});
});
});
I create mock post router handler for documents.js router. As you can see from mockImplementation for this route handler, it checks if the path is equal to '/mine' (which is my supertest endpoint), then calls console.warn('called'); and callback. When I run this test file, I can not see any yellow warning message with body 'called'. And also when POST request endpoint /api/documents/mine the server doesn't trigger my mock function documentsRouter.post. It has never been called. So I think the server's documents router is not getting replaced with my mock post route handler. It still uses original post route handler to respond my POST request. What should I do to test if my mock documentsRouter.post function have been called?
Note that my User model has a custom method for checking left disk space of user. I also tried to mock that mongoose custom method but It also doesn't work.
I have a test to test my cloudflare worker that looks like this:
const workerScript = fs.readFileSync(
path.resolve(__dirname, '../pkg-prd/worker.js'),
'utf8'
);
describe('worker unit test', function () {
// this.timeout(60000);
let worker;
beforeEach(() => {
worker = new Cloudworker(workerScript, {
bindings: {
HTMLRewriter
},
});
});
it('tests requests and responses', async () => {
const request = new Cloudworker.Request('https://www.example.com/pathname')
const response = await worker.dispatch(request);
console.log(response);
// const body = await response.json();
expect(response.status).to.eql(200);
// expect(body).to.eql({message: 'Hello mocha!'});
});
});
In my worker I do something like this:
const response = await fetch(BASE_URL, request);
const modifiedResponse = new Response(response.body, response);
// Remove the webflow badge
class ElementHandler {
element(element) {
element.append('<style type="text/css">body .w-webflow-badge {display: none!important}</style>', {html: true})
}
}
console.log(3);
return new HTMLRewriter()
.on('head', new ElementHandler()).transform(modifiedResponse);
Now when i run my test I get this error message:
● worker unit test › tests requests and responses
TypeError: Cannot read property 'transform' of undefined
at evalmachine.<anonymous>:1:1364
at FetchEvent.respondWith (node_modules/#dollarshaveclub/cloudworker/lib/cloudworker.js:39:17)
What seems to be wrong?
HTMLRewriter i created looks like this:
function HTMLRewriter() {
const elementHandler = {};
const on = (selector, handler) => {
if (handler && handler.element) {
if (!elementHandler[selector]) {
elementHandler[selector] = [];
}
elementHandler[selector].push(handler.element.bind(handler));
}
};
const transform = async response => {
const tempResponse = response.clone();
const doc = HTMLParser.parse(await tempResponse.text());
Object.keys(elementHandler).forEach(selector => {
const el = doc.querySelector(selector);
if (el) {
elementHandler[selector].map(callback => {
callback(new _Element(el));
});
}
});
return new Response(doc.toString(), response);
};
return {
on,
transform
};
}
Since HTMLRewriter() is called with new, the function needs to be a constructor. In JavaScript, a constructor function should set properties on this and should not return a value. But, your function is written to return a value.
So, try changing this:
return {
on,
transform
};
To this:
this.on = on;
this.transform = transform;
I'm using a static array to scaffold a user table, prior to refactoring with actual postgres db and some fetch()-ing code. At present, the tests work, but obviously they are working synchronously. Here's the placeholder API code:
// UserAPI.js
let findUserById = (credentials = {}) => {
const { userId } = credentials
if (userId) {
const foundUser = users.find(user => user.id === userId)
if (foundUser !== undefined) {
const { password: storedpassword, ...user } = foundUser
return user
}
}
return null
}
exports.byId = findUserById
And an example test as follows:
// excerpt from TokenAuth.test.js
const UserAPI = require('../lib/UserAPI')
describe('With TokenAuth middleware', () => {
beforeEach(() => {
setStatus(0)
})
it('should add user to req on authorised requests', () => {
const token = createToken(fakeUser)
const authReq = { headers: { authorization: 'Bearer ' + token } }
const myMiddleware = TokenAuth(UserAPI.byId)
myMiddleware(authReq, fakeRes, fakeNext)
// expect(authReq.user).toStrictEqual({ id: 1, username: 'smith#example.com' });
expect(authReq.user.username).toStrictEqual('smith#example.com')
expect(authReq.user.id).toStrictEqual(1)
})
})
This runs fine, and along with other tests gives me the coverage I want. However, I now want to check that the tests will deal with the async/await nature of the fetch() code I'm going to use for the proper UserAPI.js file. So I re-write the placeholder code as:
// UserAPI.js with added async/await pauses ;-)
let findUserById = async (credentials = {}) => {
const { userId } = credentials
// simulate url resolution
await new Promise(resolve => setTimeout(() => resolve(), 100)) // avoid jest open handle error
if (userId) {
const foundUser = users.find(user => user.id === userId)
if (foundUser !== undefined) {
const { password: storedpassword, ...user } = foundUser
return user
}
}
return null
}
exports.byId = findUserById
... at which point I start getting some lovely failures, due I think it's returning unresolved promises.
My problem is two-fold:
How should I alter the UserAPI.test.js tests to deal with the new async nature of findUserByCredentials() ?
Am I ok in my assumption that ExpressJS is happy with async functions as request handlers? Specifically, due to the async nature ofUserAPI.findUserByCredentials is this ok?
Main App.js uses curried UserAPI.byId() for the findUserById.
// App.js (massively simplified)
const express = require('express')
const TokenAuth = require('./middleware/TokenAuth')
const RequireAuth = require('./middleware/RequireAuth')
const UserAPI = require('./lib/UserAPI')
let router = express.Router()
const app = express()
app.use(TokenAuth(UserAPI.byId))
app.use(RequireAuth)
app.use('/users', UserRouter)
module.exports = app
My TokenAuth middleware would now run along these lines:
// TokenAuth.js (simplified)
const jwt = require('jsonwebtoken')
require('dotenv').config()
const signature = process.env.SIGNATURE
let TokenAuth = findUserById => async (req, res, next) => {
let header = req.headers.authorization || ''
let [type, token] = header.split(' ')
if (type === 'Bearer') {
let payload
try {
payload = jwt.verify(token, signature)
} catch (err) {
res.sendStatus(401)
return
}
let user = await findUserById(payload)
if (user) {
req.user = user
} else {
res.sendStatus(401)
return
}
}
next()
}
module.exports = TokenAuth
A partial answer us simply to add an async/await on the middleware call:
it('should add user to req on authorised requests', async () => {
const token = createToken(fakeUser)
const authReq = { headers: { authorization: 'Bearer ' + token } }
const myMiddleware = TokenAuth(UserAPI.byId)
await myMiddleware(authReq, fakeRes, fakeNext)
// expect(authReq.user).toStrictEqual({ id: 1, username: 'smith#example.com' });
expect(authReq.user.username).toStrictEqual('smith#example.com')
expect(authReq.user.id).toStrictEqual(1)
})
I have some logic inside my local login strategy for passport that I want to unit test with stubs/mocks (because it calls an external API), but I can't seem to get into the function to test it.
Here's my test.js file:
var expect = require('chai').expect;
var sinon = require('sinon');
var proxyquire = require('proxyquire');
require('../config/passport.js');
describe('it should get user account from the API', function () {
it('should be able to access passport authenticate', function(){
var reqUserObject = {
body: { user_name: 'fakeymcfakeypants', password: '123Skidoo' }
}
var requestPromiseStub = sinon.stub();
requestPromiseStub.onCall(0).returns(Promise.resolve('{"userId": 138}'))
.onCall(1).returns(Promise.resolve('{"userName": "fakeymcfakeypants", "status": 0}'))
var passportTest = proxyquire('passport', {
'request-promise': requestPromiseStub
});
var passportStub = sinon.stub(passportTest, "authenticate");
var response = passportStub.calledWith('localLogin', reqUserObject);
console.log(response);
expect.response.to.be.true;
});
});
And the setup for the function in config/passport.js:
var rp = require('request-promise');
var passport = require("passport");
var LocalStrategy = require('passport-local').Strategy;
module.exports = function (passport, LocalStrategy) {
passport.use('localLogin', new LocalStrategy({
usernameField : 'user[user_name]',
passwordField : 'user[password]'},
function(username, password, done) {
//logic blah blah blah here, uses two request-promise calls
return done(null, username);
})
)
}
As far as I can tell, the passport.authenticate method is not being called (it always returns false). If I remove proxyquire and just require passport & config/passport.js 'normally' the response is also false.
I know this is a bit complicated, so any other suggestions as to how to test this would be greatly appreciated!
EDITING TO ADD CURRENT CODE:
var expect = require('chai').expect;
var chai = require('chai');
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var passport = require('passport');
var sinonChai = require('sinon-chai');
var async = require('async');
chai.use(sinonChai);
describe('canary test: passport', function (){
it('should pass this canary test', function(){
expect(true).to.be.true;
});
});
describe('it should get user account from the API', function () {
var authSpy;
var requestPromiseStub;
var passportResponse;
var userResponse;
beforeEach(function () {
this.sandbox = sinon.sandbox.create()
authSpy = sinon.spy(passport, 'authenticate');
})
afterEach(function () {
this.sandbox.restore()
})
it('should be able to access passport authenticate', function(){
var mockReq = {
body: {
username: 'fakeymcfakeypants',
password: '123Skidoo'
},
logIn: function () {}
};
var mockRes = {
};
requestPromiseStub = sinon.stub();
next = sinon.stub();
requestPromiseStub.onCall(0).returns(Promise.resolve({userId: 138, statusCode: 200}))
.onCall(1).returns(Promise.resolve({userName: 'fakeymcfakeypants', status : 0}))
var overrides = {
'request-promise': requestPromiseStub,
'authenticate': {authenticate: authSpy}
};
proxyquire('../config/passport.js', overrides)();
//added 'next' here as authenticate expects it: https://github.com/jaredhanson/passport/blob/master/lib/middleware/authenticate.js#L81
//passport should return either a username, or false, not sure how to access that?
passport.authenticate('localLogin')(mockReq, mockRes, next);
// if I comment out the 'logIn' function above and make an explicit function here I can see the username being returned, but of course it's inside the function closure:
passport.authenticate('localLogin', function(err, user){
// I can see here that the username is correct:
console.log(user)
})(mockReq, mockRes, next);
expect(requestPromiseStub).to.have.been.called;
});
});
I'm pretty sure I'm overlooking something really simple & dumb, but I can't seem to get normal callbacky wrappers to work with the syntax of passport.authenticate. :(
config/passport.js
var rp = require('request-promise');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
module.exports = function () {
passport.use('local', new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
},
function (username, password, done) {
console.log('logic blah blah blah here, uses two request-promise calls');
return done(null, username);
}));
};
test.js
var chai = require('chai');
var expect = require('chai').expect;
var sinon = require('sinon');
var proxyquire = require('proxyquire');
var passport = require('passport');
var sinonChai = require('sinon-chai');
chai.use(sinonChai);
describe('it should get user account from the API', function () {
it('should be able to access passport authenticate', function () {
// configure request and response
var mockReq = {
body: {
username: 'johndoe',
password: 'secret'
},
logIn: function () {}
};
var mockRes = {};
// configure request-promise
var requestPromiseStub = sinon.stub();
requestPromiseStub
.onCall(0).returns(Promise.resolve({
userId: 138
}))
.onCall(1).returns(Promise.resolve({
userName: 'johndoe',
status: 0
}));
var overrides = {
'request-promise': requestPromiseStub
};
proxyquire('./passport.js', overrides)();
passport.authenticate('local')(mockReq, mockRes);
// ASSERTS HERE
//expect(requestPromiseStub).to.have.been.called();
});
});