Testing express middleware - javascript

I have following code to test:
const Status = require('http-status-codes');
const passport = require('passport');
const Users = require('../models/users.js');
const authentication = {
// Authenticate User Middleware
authenticateUser: function authenticateUser(req, res, next) {
return passport.authenticate('bearer', { session: false, failWithError: false },
(err, user, info) => {
if (err) {
return res.status(500).json({ message: err });
}
if (user) {
return Users.findOne({ auth_ref: user.auth_ref })
.populate('groups')
.exec((e, doc) => {
if (e) {
return res.status(500).json({ message: e });
}
req.authInfo = info;
req.user = doc;
return next(null, doc, info);
});
}
return res.status(Status.UNAUTHORIZED).json({ message: 'Access denied' });
}
)(req, res, next);
},
};
module.exports = authentication.authenticateUser;
My test file:
const test = require('ava');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
const Util = require('../util');
Util.beforeEach(test, (t) => {
const authenticateStub = sinon.stub();
const passportStub = {
authenticate: authenticateStub,
};
const authenticationMocked = proxyquire('../../../middleware/authentication', { passport: passportStub });
t.context.authenticateStub = authenticateStub;
t.context.authenticationMocked = authenticationMocked;
});
Util.afterEach(test);
Util.after(test);
test('[middleware/authentication] authenticateUser function call succeed', sinon.test(async (t) => {
// given
const func = t.context.authenticationMocked;
t.context.authenticateStub.withArgs(sinon.match.any, sinon.match.any, sinon.match.any).yields('error', { statusCode: 500 }, 'sampleUser');
const nextSpy = sinon.spy();
const fakeReq = { user: { email: '' } };
const res = {
status: () => res,
json: () => res,
};
// when
func(fakeReq, res, nextSpy);
// then
})
My problem is that I somehow can't mock the res parameter in a way so that no error occurs.
This code produces the following error:
Rejected promise returned by test. Reason:
TypeError {
message: 'passport.authenticate(...) is not a function', }
If I remove the res object to {} the error is res.status is not a function
Am I doing something wrong with the initialization or is my res object wrong?

I now found the following solution:
// given
const func = t.context.authenticationMocked;
t.context.authenticateStub.withArgs(sinon.match.any, sinon.match.any, sinon.match.any).yields('error', { statusCode: 500 }, 'sampleUser').returns(() => {});
const nextSpy = sinon.spy();
const fakeReq = { user: { email: '' } };
const rootRouteStub = {
status: sinon.stub(),
json: sinon.spy(),
};
rootRouteStub.status.returns(rootRouteStub);
// when
func(fakeReq, rootRouteStub, nextSpy);

Related

Getting erros using passport-google-oauth20 InternalOAuthError: Failed to fetch user profile and Cannot set headers after they are sent to the client

I'm using passport strategies for different socialMedia logins and getting the following two errors
InternalOAuthError: Failed to fetch user profile
Cannot set headers after they are sent to the client
I have doubt there somewhere I have returned a callback or response so getting 2nd error but for 1st don't know reasons scope seems to be correct!
strategy code
passport.use(new GoogleStrategy({
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_SECRET_KEY,
callbackURL: GOOGLE_CALLBACK_URL
}, async (acessToken, refreshToken, profile, done) => {
await User.findOne({ email: profile._json.email }, async (err, user) => {
if (err) {
console.log("passport.config --> err", err);
done(err, null);
} else if (user) {
if (user.socialType !== "GOOGLE" || user.socialType === null)
done(`LOGIN_CREDENTIALS_WITH_${(user.socialType || "PASSWORD").toUpperCase()}`, false);
else {
done(null, user);
}
} else {
// console.log(profile);
const user = {
email: profile._json.email,
socialId: profile.id,
socialType: "GOOGLE",
firstName: profile.name.givenName,
lastName: profile.name.familyName,
isActive: profile._json.email_verified,
isVerified: profile._json.email_verified,
socialImageUrl: profile._json.picture,
userType: "CUSTOMER"
};
const newUser = new User({ ...user });
const newUserData = await newUser.save();
done(null, newUserData);
}
});
}));
route code:
router.get('/auth/:socialType', customerCtrl.socialTypeLogin);
router.get('/auth/:socialType/callback', customerCtrl.socialTypeLoginCallback);
controller code:
const socialTypeLogin = async (req, res) => {
await customerService.socialTypeLogin(req, res);
};
const socialTypeLoginCallback = async (req,res) => {
await customerService.socialTypeLoginCallback(req,res);
};
service code:
const socialTypeLogin = async (req, res) => {
try {
const socialType = (req.params.socialType || '').toLowerCase();
const GOOGLE_SCOPE = ['email', 'profile'];
const FACEBOOK_SCOPE = ['email'];
let scope = [];
if (socialType === 'google') {
scope = GOOGLE_SCOPE;
} else if (socialType === 'facebook') {
scope = FACEBOOK_SCOPE;
}
let oauthOptions = { scope: scope};
const { returnUrl } = req.query;
if(returnUrl && returnUrl.trim().length !== 0) {
oauthOptions['state'] =JSON.stringify({ returnUrl: returnUrl });
}
passport.authenticate(socialType, oauthOptions)(req, res);
}
catch (error) {
}
}
/**
* #param {string} socialType
*/
const socialTypeLoginCallback = async (req, res) => {
const socialType = (req.params.socialType || '').toLowerCase();
// return new Promise((resolve, reject) => {
try {
passport.authenticate(socialType, async (err, user) => {
let webappRedirectURL = WEBAPP_LOGIN_URL;
try {
const state = req.query.state;
if(state) {
const stateObj = JSON.parse(state);
webappRedirectURL = stateObj.returnUrl;
}
} catch (err1) {
console.log("customer.service --> parsing error",err1);
}
if (err || !user) {
console.log("customer.service --> !user",err);
res.render('oauth-redirect', {
webappRedirectURL: webappRedirectURL,
success: false,
error: err,
timerCounter: 5,
accessToken: undefined
});
}
else {
console.log("customer.service --> Generating Token",user.generateJWT());
res.render('oauth-redirect', {
webappRedirectURL: webappRedirectURL,
success: true,
timerCounter: 5,
accessToken: user.generateJWT(),
error: undefined
});
}
})(req, res);
}
catch (error) {
console.log("customerService.js ==> socialTypeLoginCallback -->",error);
}
};
Thanks for help in advance!
I have doubt there somewhere I have returned a callback or response so getting 2nd error but for 1st don't know reasons scope seems to be correct!
In socialTypeLogin
add line
oauthOptions['session'] = false;

Return promise in node.js

i try to return data in node.js from a APIs , but I'm having problems, because I need an asynchronous function, I couldn't understand for sure the correct use of the promise, I've tried everything and I couldn't put the result in the return, only in the console.log, somebody help me?
const express = require('express')
const MFA = require('mangadex-full-api')
module.exports = {
async indexManga(req, res) {
const mangalist = MFA.login('DarksGol', 'R#ul1605', './md_cache/').then(async () => {
manga = []
await MFA.Manga.search('Kiss').then(results => {
results.forEach((elem, i) => {
let obj = {}
obj.status = elem.status
obj.title = elem.title
manga.push(obj)
})
}).catch(console.error)
return manga
}).catch(console.error)
console.log(await mangalist)
return mangalist
}
}
no error occurred, only infinite loading on request
const express = require('express')
const routes = express.Router()
const searchManga = require('../src/controllers/searchManga')
routes.get('/searchManga', searchManga.indexManga)
module.exports = routes
Looks like indexManga is an endpoint. Each endpoint function must end the request-response cycle by sending a response ( res.send(), res.json(), res.end(), etc). If indexManga s an endpoint, the solution would be:
...
//return mangalist
res.send(mangalist)
or
//return mangalist
res.json({ status: "success", message: "logged in successfully" })
If it is a meddleware:
async indexManga(req, res, next) {
...
//return mangalist
return next()
}
EDIT: you are using async/await with .then() improperly in some places.
Try this way:
module.exports = {
indexManga(req, res) {
MFA.login('DarksGol', 'R#ul1605', './md_cache/').then(() => {
manga = []
MFA.Manga.search('Kiss').then(results => {
results.forEach((elem, i) => {
let obj = {}
obj.status = elem.status
obj.title = elem.title
manga.push(obj)
})
res.json({ status: "success", data: manga })
}).catch((err) => {
res.json({ status: "fail", error: err })
})
}).catch((err) => {
res.json({ status: "fail", error: err })
})
}
}
I don't see why this would cause "infinite loading on request", but you could greatly simplify the code to just
const express = require('express');
const MFA = require('mangadex-full-api');
module.exports = {
async indexManga(req, res) {
await MFA.login('DarksGol', 'R#ul1605', './md_cache/')
const mangaList = [];
const results = await MFA.Manga.search('Kiss');
results.forEach(elem => {
mangaList.push({
status: elem.status,
title: elem.title,
});
});
return mangalist;
},
};

How can I get my err.message in the alert for this node.js app?

I have a PUT in a REST API that should display an error message that says "upvoted already" if the vote_score is 1 (that is, they voted already), but instead I get a generic "internal server error" message in alert which is not good UX. That's always what the error will say with what I have tried so far.
How can I get my error message to display as "upvoted already"? Or for that matter, how can I get any error message to show up with a message? I hope I have provided enough information with the API code followed with the front-end code.
What I have tried thus far is trying different things like res.status(200).json({ error: err.toString() }); and next(err).
Hopefully something simple, I am hoping for a ELI5 type answer because I am a beginner and my error-handling game is weak. Thanks.
const db = require('../db');
const express = require('express');
const debug = require('debug')('app:api:vote');
const Joi = require('joi');
const auth = require('../middleware/auth');
const admin = require('../middleware/admin');
const { required } = require('joi');
const router = express.Router();
router.use(express.urlencoded({ extended: false }));
router.use(express.json());
// general error handler
const sendError = (err, res) => {
debug(err);
if (err.isJoi) {
res.json({ error: err.details.map((x) => x.message + '.').join('\n') });
} else {
res.json({ error: err.message });
}
};
router.put('/upvote/:emojiId/', auth, async (req, res, next) => {
try {
const schema = Joi.object({
emoji_id: Joi.number().required(),
user_id: Joi.number().required(),
vote_score: Joi.number(),
});
const vote = await schema.validateAsync({
emoji_id: req.params.emojiId,
user_id: req.user.user_id,
vote_score: 1,
});
if (!(await db.findVoteByUser(vote.emoji_id, vote.user_id))) {
const upvote = await db.upvote(vote);
} else if ((await db.findVoteByUser(vote.emoji_id, vote.user_id)) == 1) {
throw new Error('Upvoted already');
}
const upvoteScore = await db.getJustUpvotesForEmoji(vote.emoji_id);
res.json(upvoteScore);
} catch (err) {
res.status(500).json({ error: err.toString() });
}
});
module.exports = router;
And the front-end...
$(document).on('click', '.upvote-emoji-button', (evt) => {
const button = $(evt.currentTarget);
const emoji_id = button.data('id');
$.ajax({
method: 'PUT',
url: `/api/vote/upvote/${emoji_id}`,
data: emoji_id,
dataType: 'json',
})
.done((res) => {
if (res.error) {
bootbox.alert(res.error);
} else {
// $('#search-emoji-form').trigger('submit');
button.addClass('btn-danger').removeClass('btn-primary');
button.parent().next().next().html(res.upvotes);
button.parent().next().next().next().next().html(res.vote_count);
button.parent().next().next().next().next().next().html(res.total_score);
}
})
.fail((xhr, textStatus, err) => {
bootbox.alert(err);
});
});
try to replace
res.status(500).json({ error: err.toString() });
with
res.status(400).send(err.toString());
Documentation
Here is what I ended up doing. It took care of my error and a few other things too. :)
//setup
const db = require('../db');
const express = require('express');
const debug = require('debug')('app:api:vote');
const Joi = require('joi');
const auth = require('../middleware/auth');
const admin = require('../middleware/admin');
const { required } = require('joi');
const router = express.Router();
router.use(express.urlencoded({ extended: false }));
router.use(express.json());
// general error handler
const sendError = (err, res) => {
debug(err);
if (err.isJoi) {
res.json({ error: err.details.map((x) => x.message + '.').join('\n') });
} else {
res.json({ error: err.message });
}
};
router.put('/upvote/:emojiId/', auth, async (req, res, next) => {
let vote = {};
try {
const schema = Joi.object({
emoji_id: Joi.number().required(),
user_id: Joi.number().required(),
vote_score: Joi.number(),
});
vote = await schema.validateAsync({
emoji_id: req.params.emojiId,
user_id: req.user.user_id,
vote_score: 1,
});
if (!(await db.findUserByID(req.user.user_id))) {
throw new Error('log in again.');
}
const tester = await db.findVoteByUser(vote.user_id, vote.emoji_id);
if (!(await db.findVoteByUser(vote.user_id, vote.emoji_id))) {
await db.upvotePost(vote);
const upvoteScore = await db.getJustUpvotesForEmoji(vote.emoji_id);
const message = 'message';
upvoteScore[message] = 'Upvote sent.';
const action = 'action';
upvoteScore[action] = 1;
res.json(upvoteScore);
} else if (tester.vote_score == -1) {
await db.upvotePut(vote);
const upvoteScore = await db.getJustUpvotesForEmoji(vote.emoji_id);
const message = 'message';
upvoteScore[message] = 'Downvote changed to upvote.';
const action = 'action';
upvoteScore[action] = 2;
res.json(upvoteScore);
} else {
await db.deleteVoteByUserIdAndEmojiId(vote);
const upvoteScore = await db.getJustUpvotesForEmoji(vote.emoji_id);
const message = 'message';
upvoteScore[message] = 'Upvote deleted.';
const action = 'action';
upvoteScore[action] = 3;
res.json(upvoteScore);
}
} catch (err) {
sendError(err, res);
}
});
module.exports = router;
and front end..
$(document).on('click', '.upvote-emoji-button', (evt) => {
const button = $(evt.currentTarget);
const emoji_id = button.data('id');
$.ajax({
method: 'PUT',
url: `/api/vote/upvote/${emoji_id}`,
data: emoji_id,
dataType: 'json',
})
.done((res) => {
if (res.error) {
bootbox.alert(res.error);
} else {
if (res.action == 1) {
button.addClass('btn-danger').removeClass('btn-primary');
button.parent().next().next().html(res.upvotes);
button.parent().next().next().next().next().html(res.vote_count);
button.parent().next().next().next().next().next().html(res.total_score);
bootbox.alert(res.message);
} else if (res.action == 2) {
button.addClass('btn-danger').removeClass('btn-primary');
button.parent().next().children().addClass('btn-primary').removeClass('btn-danger');
button.parent().next().next().html(res.upvotes);
button.parent().next().next().next().next().html(res.vote_count);
button.parent().next().next().next().next().next().html(res.total_score);
bootbox.alert(res.message);
} else if (res.action == 3) {
button.removeClass('btn-danger').addClass('btn-primary');
button.parent().next().next().html(res.upvotes);
button.parent().next().next().next().next().html(res.vote_count);
button.parent().next().next().next().next().next().html(res.total_score);
bootbox.alert(res.message);
}
}
})
.fail((xhr, textStatus, err) => {
bootbox.alert(err);
// alert(`${textStatus}\n${err}\n${xhr.status}`);
});
});

Cannot Stub Function Returning Promise

I was trying to stub an arrow function removeUserEntry, but when executing acctRmRouter in the test, my stub seems being ignored. I have to explicitly stub the UserModel's deleteOne method to get the test successfully, I am wondering why the ignorance happens, thank you
acctRoute.js
const removeUserEntry = (username) => {
const condition = {username: username};
return UserModel.deleteOne(condition)
.then((res) => {
if (res.n < 1) {
throw new Error('User not exists');
}
return true;
}, (err) => {throw err})
.catch(err => err);
};
const acctRmRouter = function(httpReq, httpRes, next) {
if (!argValidate(httpReq.body, 'string')) {
httpRes.locals = {api: { success: false }};
// return to avoid running downwards
return next(new Error('bad argument'));
}
// perform DB rm user
return removeUserEntry(httpReq.body).then((res) => {
if (res === true) {
httpRes.locals = {api: { success: true }};
next();
} else {
httpRes.locals = {api: { success: false }}
next(res);
}
});
};
acctRoute.spec.js
it('should remove user handler pass success request', async () => {
shouldDbReset = false;
const mockRequestURL = "/api/account/rm-user";
const mockRequest = httpMocks.createRequest({
method: "POST",
url: mockRequestURL,
headers: {
"Content-Type": "text/plain"
},
body: 'validRmUser',
});
const mockResponse = httpMocks.createResponse();
const spyNext = sinon.spy();
const stubRemoveUserEntry = sinon.stub(accountRouterHelper, 'removeUserEntry');
stubRemoveUserEntry.callsFake(function(){
return Promise.resolve(true);
}); // Expecting this function to be stubbed, and always return true
await accountRouterHelper.acctRmRouter(mockRequest, mockResponse, spyNext);
/* But when running the function, it returns error object with "User not exists"
which is not what intended */
const firstCallArgs = spyNext.getCall(0).args[0];
expect(spyNext.called).to.be.true;
console.log(`firstCallArgs: ${firstCallArgs}`)
expect(firstCallArgs instanceof Error).to.be.false;
expect(spyNext.args[0].length).to.equal(0);
expect(mockResponse.statusCode).to.equal(200);
expect(mockResponse.locals.api.success).to.be.true;
stubRemoveUserEntry.resetHistory();
stubRemoveUserEntry.restore();
});
The following indeed stubbed successfully with similar pattern to removeUserEntry.
acctRoute.js
const createUserEntry = (userData) => {
const updatedUserData = filterInput(userData);
const userDoc = new UserModel(updatedUserData);
return userDoc.save()
.then((userObj) => userObj._doc
,(err) => { throw err;})
.catch(err => err);
};
const acctCreateRouter = function (httpReq, httpRes, next) {
// do something in mongodb
return createUserEntry(userCondition)
.then((response) => {
if (!(response instanceof Error)) {
httpRes.locals = {api: { success: true}};
next();
} else {
httpRes.locals = {api: { success: false}};
next(response);
}
}, (err) => {
httpRes.locals = {api: { success: false}};
next(err);
})
.catch((err) => {
httpRes.locals = {api: { success: false}};
next(err);
});
};
const acctOutputRouter = function(req, res, next) {
if (res.locals) {
res.send(res.locals.api);
} else {next()}
};
acctRoute.spec.js
it("should return and save the success result to response locals for next route", () => {
shouldDbReset = false;
const mockResponse = httpMocks.createResponse();
const stubCreateUserEntry = sinon.stub(accountRouterHelper, 'createUserEntry');
const mockNext = sinon.spy();
stubCreateUserEntry.callsFake(function(){
return Promise.resolve();
}); // Unlike removeUserEntry, stubbing neatly with desired output
return accountRouterHelper.acctCreateRouter(mockRequest, mockResponse, mockNext)
.then(() => {
expect(mockNext.called).to.be.true;
expect(mockResponse.locals.api.success).to.be.true;
})
.finally(() => {
mockNext.resetHistory();
stubCreateUserEntry.restore();
});
});
Issue
sinon.stub(accountRouterHelper, 'removeUserEntry') replaces the module export.
acctRmRouter() is not calling the module export, it is calling removeUserEntry() directly so stubbing the module export does nothing.
Solution
Refactor acctRmRouter() to call the module export for removeUserEntry().
ES6
// import module into itself
import * as self from './acctRoute';
...
const acctRmRouter = function(httpReq, httpRes, next) {
...
// call the function using the module
return self.removeUserEntry(httpReq.body).then((res) => {
...
Node.js module
...
const acctRmRouter = function(httpReq, httpRes, next) {
...
// call the function using module.exports
return module.exports.removeUserEntry(httpReq.body).then((res) => {
...

Testing Middleware and get TypeError: is not a function

I'm doing tests for middleware function and get an error:
TypeError: *** is not a function
My test.js
describe('Login', () => {
it('it must check that function create the token', () => {
const req = {
body: { email: 'user#mail.com', password: '12345' }
}
const res = { locals: sinon.spy() }
return authMiddleware.login(req, res) // authMiddleware.test.js:41
.then(res => {
expect(res.locals.token).to.not.be.undefined;
})
.catch(err => console.log(err));
});
});
and middleware.js
module.exports = User => ({
login(req, res, next) {
if (!req.body.email || !req.body.password) return res.sendStatus(401);
return User.findOne({ email: req.body.email })
.then(user => {
if (!user) return res.sendStatus(401);
if (!user.isValidPassword(req.body.password)) return
res.sendStatus(401);
let payload = { id: user.id };
let token = jwt.encode(payload, config.auth.jwtSecret);
res.locals.token = token;
next();
})
.catch(next);
},
});
Error:
TypeError: authMiddleware.login is not a function
at Context.it (test/api/middleware/authMiddleware.test.js:41:35)
What am I doing wrong?
Your middleware.js file exports an arrow function that accepts the User parameter. The return value of this function is an object which has a login method with an express middleware signature.
You should invoke login middleware like this:
const { login: loginMiddleware } = require('./middleware')(req.body);
loginMiddleware(req, res, next);

Categories