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();
});
});
Related
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.
Below is the code I am trying: index.jsand its not working while changing the password in terms of salt and hash.(saving them in database) I am keep getting the error as setPassword is not defined. Also I think I am committing code errors as well. I want the exact route code for change password using 'passport-local' Strategy.
P.S. I am able to successfully register the user and login as well. I just want to give him the option to change the password.
var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
User.findOne({id: req.user.id}, function (err, data) {
console.log("came inside api changePassword else condition inside User.findOne");
if (err) {
console.log(err);
}
else {
data.setPassword(req.body.newPass, function(err,datas){
if(datas) {
data.save(function (err,datass) {
if (err) {
res.render('settingsClient', {errorMessages: err});
} else {
console.log("Hash and Salt saved");
}
});
}
else {
console.log("setPassword error"+ err);
}
});
}
})
This is
Models (user.js) with which I am saving the password at the start of registration of user as hash and salt.
var mongoose = require('mongoose');
var crypto = require('crypto');
var userSchema = new mongoose.Schema({
email: {
type: String,
unique: true,
required: true
},
name: {
type: String,
required: true
},
hash: String,
salt: String
});
userSchema.methods.setPassword = function(password) {
this.salt = crypto.randomBytes(16).toString('hex');
this.hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha1').toString('hex');
};
userSchema.methods.validPassword = function(password) {
var hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha1').toString('hex');
return this.hash === hash;
};
module.exports = mongoose.model('User', userSchema);
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 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
Using ExpressJs 4 and Primus, I can share the Express session.
primus.on('connection', function (spark) {
var req = spark.request; // Here, I have the Express session
req.session.foo = 'bar'; // I try to write into the session.
});
When I write the { foo: 'bar' } value into the session, I can't retrieve it from a standard http express call.
app.get('/api/...', function (req, res) {
console.log(req.session.foo); // Print undefined :'(
res.send();
});
The output is undefined. Could you explain me why?
Thanks.
According to the express-session documentation, we can save the session to the store.
https://github.com/expressjs/session#sessionsave
primus.on('connection', function (spark) {
var req = spark.request; // Here, I have the Express session
req.session.foo = 'bar'; // I try to write into the session.
req.session.save(); // Save the session to the store
});
Then, it works well!
Here's how I have it working currently (Express 4.13.4; Primus 4.0.5)
App.js
var express = require('express');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var sockjs = require('sockjs');
var http = require('http');
var Primus = require('primus');
var realtime_functions = require('./custom_modules/realtime_functions.js');
var app = express();
var app_secret_key = 'app secret';
var cookieparser = cookieParser(app_secret_key);
var sessionstore = new session.MemoryStore();
app.use(cookieparser);
app.use(session({
secret: app_secret_key,
saveUninitialized: false,
resave: false,
store: sessionstore
}));
realtime_functions(app, cookieparser, sessionstore);
realtime_functions.js
var primus_start = function(express, cookies, store) {
var server = http.createServer(express);
var primus = new Primus(server, {transformer: 'SockJS', parser: 'JSON'})
primus.use('rooms', primusRooms)
server.listen(3000, '0.0.0.0');
primus.before('cookies', cookies);
primus.before('session', function session(req, res, next) {
try {
var sid = req.signedCookies['connect.sid'];
if (!sid) { return next(); }
req.pause();
store.get(sid, function (err, session) {
if (err) {
primus.emit('log', 'error', err);
return next();
}
if(session) {
req.sessionID = sid;
req.sessionStore = store;
req.thesession = store.createSession(req, session);
}
req.resume();
next();
});
} catch(error) {
console.log(error);
}
});
primus.on('connection', function(spark) {
spark.on('data', function(data) {
spark.request.thesession.addthis = "save this to session";
spark.request.thesession.save();
});
});
}
module.exports = primus_start;