Jest Memory Leak testing Express Middleware - node.js

I am attempting to unit test my authentication middleware for Express. The middleware is quite simple and can be viewed below in its entirety:
const admin = require('./../config/firebase/firebase');
// Models - User
const User = require('./../models/user');
const auth = async (req, res, next) => {
try {
// The Authorization Bearer Token sent in the header of the request needs to be decoded.
const token = req.header('Authorization').replace('Bearer ', '');
const decoded = await admin.auth().verifyIdToken(token);
// Finding that user in the database by their Firebase UID.
const user = await User.findOne({ _id: decoded.uid });
// If that user does not exist, we'll throw an error.
if (!user) {
throw new Error();
}
// Making the user accessible to the endpoint.
req.user = user;
// Proceed
next();
} catch (e) {
// HTTP 404 Unauthorized Response Status
res.status(401).send({ error: 'Please authenticate.' });
}
}
module.exports = auth;
Since the Firebase Admin SDK returns an object that contains the user's UID as a property, for the purpose of my tests, I create a "fake token" which is just an object with a UID property. I then mock the Admin SDK such that it returns whatever was passed in, as so:
module.exports = {
auth() {
return this;
},
verifyIdToken(token) {
return JSON.parse(token);
},
initializeApp(app) {
},
credential: {
cert() {
}
}
}
Since the auth middleware expects to find a user in the test database, I have to configure that as Jest Setup in the beforeAll hook:
const userOneToken = JSON.stringify({ uid: 'example UID' });
const userOne = {
_id: 'example UID',
// ...
};
beforeAll(async () => {
await User.deleteMany();
await User.save(userOne);
app.use(auth).get('/', (req, res) => res.send());
});
This means that the middleware will always be able to get a UID in return, which can be used to find a test user in the test database.
The test suite itself, after importing my Express Application, is quite simple, with just three tests:
const auth = require('./../../src/middleware/auth');
describe('Express Auth Middleware', () => {
test('Should return 401 with an invalid token', async () => {
await request(app)
.get('/')
.set('Authorization', 'Bearer 123')
.send()
.expect(401);
});
test('Should return 401 without an Authorization Header', async () => {
await request(app)
.get('/')
.send()
.expect(401);
});
test('Should return 200 with a valid token', async () => {
await request(app)
.get('/')
.set('Authorization', `Bearer ${userOneToken}`)
.send()
.expect(200);
});
});
It appears, however, that the tests are leaking memory (apparent by calling with the --detectLeaks flag). Additionally, it seems Jest is also finding an open handle left behind by the last test. Running the suite with the --detectOpenHandles flag returns a TCPSERVERWRAP error on the get request of the last test.
Potential solutions were proposed in this GitHub issue, but none of them worked for me.
Any help solving this issue would be much appreciated, for all my test suites are leaking memory because they rely on Supertest. Thank you.

Related

How can I test authentication middleware with Jest

I'm learning nodejs and for the most part its going well. Im trying to learn how to do mocking in tests with jest. I've watched numerous tutorials but I cant seem to get my head around it.
I have this middleware that is used on protected routes...
import jwt from 'jsonwebtoken';
export default function (req, res, next) {
const token = req.header('x_auth-token');
if (!token) return res.status(401).json({ message: 'Access denied' });
try {
const verified = jwt.verify(token, process.env.TOKEN_SECRET);
req.user = verified;
next();
} catch (err) {
return res.status(400).send('Invalid Token');
}
}
From what I've read, I think the approach Im supposed to to take is something like this...
import verifyToken from '../middleware/verifyToken';
test('verifyToken', () => {
expect.assertions(1);
const res = {};
const req = {};
const next = (err) => expect(err).toBeFalsy();
verifyToken(req, res, next);
});
However this clearly doesnt work.
So how do I mock the request header with a token?
So if we comletely forget about what req, res are in the real world, obviously they are request and response, but lets just forget about that for now.
in your real code you have token = req.header("x_auth-token")
So in our test code, we need to have something in the req object that when called with those parameters returns what you want.
So I would say.
const req = {
header: jest.fn(() => 'myAuthToken')
}
the jest.fn() has mad a mock function and when it is invoked it will always return the string myAuthToken.
We can then check that the header function has been called with the correct params by adding
expect(req.header).toHaveBeenCalledWith("x_auth-token")
You are also going to need to mock jwt.verify as you are not testing that jwt.verify works as that will be covered it its own tests. You will want to make sure that you are using the response of that correctly
To do that take a look at this stack overflow question
And finally I would set next as a mock function
mockNext = jest.fn()
Then we can say in the test
expect(mockNext).toHaveBeenCalled()
So I was having trouble understandin how to mock functions. I did a bunch of googling based on on Ollie Pugh's answer and this is what I came up with.
import jwt from 'jsonwebtoken';
import verifyToken from '../middleware/verifyToken';
import { uIds } from './testdata/userTestData';
describe('verifyToken tests', () => {
const { uid1 } = uIds;
it('Should pass the userId to the request object if token is verified', () => {
const res = {};
const req = {
header: jest.fn(() => 'myAuthToken'),
};
const next = jest.fn();
const verify = jest
.spyOn(jwt, 'verify')
.mockReturnValueOnce({ userId: String(uid1) });
verifyToken(req, res, next);
expect(req.header).toHaveBeenCalledWith('x_auth-token');
expect(req.user).toEqual({ userId: String(uid1) });
expect(next).toHaveBeenCalled();
});
it('Should deny access if token is not present in header', () => {
const res = {
json(msg) {
expect(msg).toEqual({ message: 'Access denied' });
},
status(responseStatus) {
expect(responseStatus).toEqual(401);
return this;
},
};
const req = {
header: jest.fn(),
};
const next = jest.fn();
verifyToken(req, res, next);
expect(req.header).toHaveBeenCalledWith('x_auth-token');
expect(req.user).not.toEqual({ userId: String(uid1) });
});
it('Should deny access if token is invalid', () => {
const res = {
send(text) {
expect(text).toEqual('Invalid Token');
},
status(responseStatus) {
expect(responseStatus).toEqual(400);
return this;
},
};
const req = {
header: jest.fn(() => 123),
};
const next = jest.fn();
const verify = jest.fn();
verifyToken(req, res, next);
expect(req.header).toHaveBeenCalledWith('x_auth-token');
expect(req.user).not.toEqual({ userId: String(uid1) });
});
});
This passes the tests. However I'm not sure about the validity of the test.

How to mock req.session in express while testing API with jest and supertest?

What I'm trying to do is to test a REST API endpoint that validates requests with req.session (set by express-session).
Here are snippets of my code:
middleware.ts
export const validateSecurityCode = (req, res, next) => {
// How do I mock/spy on this "req.session.securityCode"?
if (req.session.securityCode !== req.body.securityCode) {
throw new BadRequestError('Wrong security code was provided');
}
return next();
};
api/item.ts
router.post('/item', [validateSecurityCode], async (req, res) => {
await createItem(req.body);
res.send({ message: 'DONE' });
});
As you can see, if req.session.securityCode !== req.body.securityCode, this endpoint has to throw an error.
Is it possible to mock/spy on req.session while testing the endpoint with jest and supurtest?
Below is what I'd like to achieve:
it('should pass security code check', async () => {
// " MOCK_REQ_SESSION('valid-code')" will ensure that "req.session.securityCode" will get 'valid-code'
validateSecurityCode = MOCK_REQ_SESSION('valid-code');
const response = await request(app).post('/api/item').send({
securityCode: 'valid-code',
// other request body values
});
expect(response.statusCode).toEqual(200);
});

.save() is not a function (Mongoose)

So, I'm not 100% why this isn't working as intended. I have an Edit Profile React component (I'm learning how to build a SSR-based application currently, using the MERN stack) - but when I submit the edit, I get an error that "user.save is not a function - Code:
From the routes:
router.route('/api/users/:userId')
.get(authCtrl.requireSignin, userCtrl.read)
.put(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.update)
.delete(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.remove)
The API Helper:
const update = async (params, credentials, user) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'PUT',
headers: {
"Accept": 'application/json',
Authorization: 'Bearer ' + credentials.t
},
body: user
})
return await response.json()
} catch (err) {
console.log(err)
}
}
And lastly, the actual controller, that handles all the logic behind the update: (This function sanitizes the password information before passing it back to the client, hence the undefineds)
const update = (req, res) => {
let form = new formidable.IncomingForm()
form.keepExtensions = true
form.parse(req, async (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Photo could not be uploaded"
})
}
let user = req.profile
user = extend(user, fields)
user.updated = Date.now()
if(files.photo){
user.photo.data = fs.readFileSync(files.photo.path)
user.photo.contentType = files.photo.type
}
try {
await user.save()
user.hashed_password = undefined
user.salt = undefined
res.json(user)
} catch (err) {
console.log(err)
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
})
}
This isn't a production level application, just for me learning how to do this from scratch (without CRA, and all contained in one project using SSR)
EDIT: After some digging, console.logs and console.dirs, I discovered that the updates passed from the component aren't even being passed to the controller. The stale data (from the database) are logging, but req.profile is completely empty. I may re-visit this code completely and make some major changes to it.. All part of learning, right?
Here are the auth methods that were requested (I'm using Session Storage for now, but that may change to localStorage):
import User from '../models/user.model'
import jwt from 'jsonwebtoken'
import expressJwt from 'express-jwt'
import config from './../../config/config'
const signin = async (req, res) => {
try {
let user = await User.findOne({email: req.body.email})
if (!user) {
return res.status(401).json({error: "User not found"})
}
if (!user.authenticate(req.body.password)) {
return res.status(401).send({error: "Email and Password do not match"})
}
const token = jwt.sign({_id: user._id}, config.jwtSecret)
res.cookie('t', token, {expire: new Date() + 9999})
return res.json({
token,
user: {
_id: user._id,
name: user.name,
email: user.email
}
})
} catch (err) {
return res.status(401).json({error: "Could not sign in"})
}
}
const signout = (req, res) => {
res.clearCookie('t')
return res.status(200).json({message: "Signed out"})
}
const requireSignin = expressJwt({
secret: config.jwtSecret,
algorithms: ['sha1', 'RS256', 'HS256'],
userProperty: 'auth'
})
const hasAuthorization = (req, res, next) => {
const authorized = req.profile && req.auth
&& req.profile._id == req.auth._id
if (!(authorized)) {
return res.status(403).json ({error: "User is not authorized"})
}
next()
}
export default {
signin,
signout,
requireSignin,
hasAuthorization
}
Possible places where you could have a mistake: (code is not shown)
If your req.profile isn't a mongoose object, this won't work
let user = req.profile
From your other posts, I think you're probably getting req.profile from your jwt. That means this is not a mongoose object. What you'll need to do is either:
As you mentioned, use findByIdAndUpdate passing the id and the object to be updated. Note that if you have a mongoose middleware for save it won't run here
Do a user = await User.findById(id), update the user as you see fit, then use user.save. This gives you a bit more control over it, but runs 2 operations.
This has been solved.. My issue was apparently with the form not passing the request body properly to the API, which was caused by a faulty install of a dependency. Once I got that solved, the rest fell into place, and I can now do what I need to do with ease..
Thank you all who attempted to troubleshoot this with me.

Mocking in jest and express

I have some code in an Express route which talks to AWS Cognito and am having trouble working out how to mock it in tests.
cognitoExpress.validate(accessTokenFromClient, (err, response) => {
if (err) return res.status(401).json({ error: err });
res.json({ data: `Hello ${response.username}!` });
});
Then in my test I want to say cognitoExpress.validate should be called once and return {username: 'test user'} so that it doesnt hit the network and doesnt actually call AWS Cognito
it('It should returns 200 with a valid token', async done => {
const { cognitoExpress } = require('../helpers/cognitoExpress');
// I have tried
jest.mock('../helpers/cognitoExpress');
// and this
jest.mock('../helpers/cognitoExpress', () => ({
validate: jest.fn()
}));
const token = 'sfsfdsfsdfsd';
const response = await request.get('/').set('Authorization', token);
expect(cognitoExpress.validate).toHaveBeenCalledWith(token);
expect(response.body).toEqual({ data: 'Hello test user' });
done();
});
Thanks in advance....
let spyInstance = undefined;
beforeAll(() => {
spyInstance = jest.spyOn(cognitoExpress.prototype, "validate").mockImplementation(() => {
// Replace the body of 'validate' here, ensure it sets
// response body to {username: 'test user'} without calling AWS
...
});
});
afterAll(() => {
expect(spyInstance).toBeDefined();
expect(spyInstance).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
});
it("It should call mocked cognitoExpress.validate once", async done => {
...
});
A similar and working test in my project. Instead of cognitoExpress.validate it mocks and tests SampleModel.getData
Create file ../helpers/__mocks__/cognitoExpress.js with mocked function you want to use. It is essential to call the folder __mocks__. You can modify a functions and return any data you want.
example
module.exports = {
validate: () => { username: 'test user' }
}
Now you can use jest.mock('../helpers/cognitoExpress'), but I recommend you to place it to some global or test setup file, not to separate tests.
Jest Manual Mocks

How to authenticate Supertest requests with Passport?

I'm using Passport.js for authentication (local strategy) and testing with Mocha and Supertest.
How can I create a session and make authenticated requests with Supertest?
As zeMirco points out, the underlying superagent module supports sessions, automatically maintaining cookies for you. However, it is possible to use the superagent.agent() functionality from supertest, through an undocumented feature.
Simply use require('supertest').agent('url') instead of require('supertest')('url'):
var request = require('supertest');
var server = request.agent('http://localhost:3000');
describe('GET /api/getDir', function(){
it('login', loginUser());
it('uri that requires user to be logged in', function(done){
server
.get('/api/getDir')
.expect(200)
.end(function(err, res){
if (err) return done(err);
console.log(res.body);
done()
});
});
});
function loginUser() {
return function(done) {
server
.post('/login')
.send({ username: 'admin', password: 'admin' })
.expect(302)
.expect('Location', '/')
.end(onResponse);
function onResponse(err, res) {
if (err) return done(err);
return done();
}
};
};
You should use superagent for that. It is lower level module and used by supertest. Take a look at the section Persisting an agent:
var request = require('superagent');
var user1 = request.agent();
user1
.post('http://localhost:4000/signin')
.send({ user: 'hunter#hunterloftis.com', password: 'password' })
.end(function(err, res) {
// user1 will manage its own cookies
// res.redirects contains an Array of redirects
});
Now you can use user1 to make authenticated requests.
Try this,
var request=require('supertest');
var cookie;
request(app)
.post('/login')
.send({ email: "user#gluck.com", password:'password' })
.end(function(err,res){
res.should.have.status(200);
cookie = res.headers['set-cookie'];
done();
});
//
// and use the cookie on the next request
request(app)
.get('/v1/your/path')
.set('cookie', cookie)
.end(function(err,res){
res.should.have.status(200);
done();
});
As an addendum to Andy's answer, in order to have Supertest startup your server for you, you can do it like this:
var request = require('supertest');
/**
* `../server` should point to your main server bootstrap file,
* which has your express app exported. For example:
*
* var app = express();
* module.exports = app;
*/
var server = require('../server');
// Using request.agent() is the key
var agent = request.agent(server);
describe('Sessions', function() {
it('Should create a session', function(done) {
agent.post('/api/session')
.send({ username: 'user', password: 'pass' })
.end(function(err, res) {
expect(req.status).to.equal(201);
done();
});
});
it('Should return the current session', function(done) {
agent.get('/api/session').end(function(err, res) {
expect(req.status).to.equal(200);
done();
});
});
});
I'm sorry, but neither of suggested solutions doesn't work for me.
With supertest.agent() I can't use the app instance, I'm required to run the server beforehand and specify the http://127.0.0.1:port and moreover I can't use supertest's expectations (assertions), I can't use the supertest-as-promised lib and so on...
The cookies case won't work for me at all.
So, my solution is:
If you are using Passport.js, it utilizes the "Bearer token" mechanism and you can use the following examples in your specs:
var request = require('supertest');
var should = require('should');
var app = require('../server/app.js'); // your server.js file
describe('Some auth-required API', function () {
var token;
before(function (done) {
request(app)
.post('/auth/local')
.send({
email: 'test#example.com',
password: 'the secret'
})
.end(function (err, res) {
if (err) {
return done(err);
}
res.body.should.to.have.property('token');
token = res.body.token;
done();
});
});
it('should respond with status code 200 and so on...', function (done) {
request(app)
.get('/api/v2/blah-blah')
.set('authorization', 'Bearer ' + token) // 1) using the authorization header
.expect(200)
.expect('Content-Type', /json/)
.end(function (err, res) {
if (err) {
return done(err);
}
// some `res.body` assertions...
done();
});
});
it('should respond with status code 200 and so on...', function (done) {
request(app)
.get('/api/v2/blah-blah')
.query({access_token: token}) // 2) using the query string
.expect(200)
.expect('Content-Type', /json/)
.end(function (err, res) {
if (err) {
return done(err);
}
// some `res.body` assertions...
done();
});
});
});
You may want to have a helper function to authenticate users:
test/auth-helper.js
'use strict';
var request = require('supertest');
var app = require('app.js');
/**
* Authenticate a test user.
*
* #param {User} user
* #param {function(err:Error, token:String)} callback
*/
exports.authenticate = function (user, callback) {
request(app)
.post('/auth/local')
.send({
email: user.email,
password: user.password
})
.end(function (err, res) {
if (err) {
return callback(err);
}
callback(null, res.body.token);
});
};
Have a productive day!
I'm going to assume that you're using the CookieSession middleware.
As grub mentioned, your goal is to get a cookie value to pass to your request. However, for whatever reason (at least in my testing), supertest won't fire 2 requests in the same test. So, we have to reverse engineer how to get the right cookie value. First, you'll need to require the modules for constructing your cookie:
var Cookie = require("express/node_modules/connect/lib/middleware/session/cookie")
, cookieSignature = require("express/node_modules/cookie-signature")
Yes, that's ugly. I put those at the top of my test file.
Next, we need to construct the cookie value. I put this into a beforeEach for the tests that would require an authenticated user:
var cookie = new Cookie()
, session = {
passport: {
user: Test.user.id
}
}
var val = "j:" + JSON.stringify(session)
val = 's:' + cookieSignature.sign(val, App.config.cookieSecret)
Test.cookie = cookie.serialize("session",val)
Test.user.id was previously defined in the portion of my beforeEach chain that defined the user I was going to "login". The structure of session is how Passport (at least currently) inserts the current user information into your session.
The var val lines with "j:" and "s:" are ripped out of the Connect CookieSession middleware that Passport will fallback on if you're using cookie-based sessions. Lastly, we serialize the cookie. I put "session" in there, because that's how I configured my cookie session middleware. Also, App.config.cookieSecret is defined elsewhere, and it must be the secret that you pass to your Express/Connect CookieSession middleware. I stash it into Test.cookie so that I can access it later.
Now, in the actual test, you need to use that cookie. For example, I have the following test:
it("should logout a user", function(done) {
r = request(App.app)
.del(App.Test.versionedPath("/logout"))
.set("cookie", Test.cookie)
// ... other sets and expectations and your .end
}
Notice the call to set with "cookie" and Test.cookie. That will cause the request to use the cookie we constructed.
And now you've faked your app into thinking that user is logged in, and you don't have to keep an actual server running.
Here is a neat approach which has the added benefit of being reusable.
const chai = require("chai")
const chaiHttp = require("chai-http")
const request = require("supertest")
const app = require("../api/app.js")
const should = chai.should()
chai.use(chaiHttp)
describe("a mocha test for an expressjs mongoose setup", () => {
// A reusable function to wrap your tests requiring auth.
const signUpThenLogIn = (credentials, testCallBack) => {
// Signs up...
chai
.request(app)
.post("/auth/wizard/signup")
.send({
name: "Wizard",
...credentials,
})
.set("Content-Type", "application/json")
.set("Accept", "application/json")
.end((err, res) => {
// ...then Logs in...
chai
.request(app)
.post("/auth/wizard/login")
.send(credentials)
.set("Content-Type", "application/json")
.set("Accept", "application/json")
.end((err, res) => {
should.not.exist(err)
res.should.have.status(200)
res.body.token.should.include("Bearer ")
// ...then passes the token back into the test
// callBack function.
testCallBack(res.body.token)
})
})
}
it.only("flipping works", done => {
// "Wrap" our test in the signUpThenLogIn function.
signUpLogIn(
// The credential parameter.
{
username: "wizard",
password: "youSHALLpass",
},
// The test wrapped in a callback function which expects
/// the token passed back from when signUpLogIn is done.
token => {
// Now we can use this token to run a test...
/// e.g. create an apprentice.
chai
.request(app)
.post("/apprentice")
.send({ name: "Apprentice 20, innit" })
// Using the token to auth!
.set("Authorization", token)
.end((err, res) => {
should.not.exist(err)
res.should.have.status(201)
// Yep. apprentice created using the token.
res.body.name.should.be.equal("Apprentice 20, innit")
done()
})
}
)
})
})
BONUS MATERIAL
To make it even more reusable, put the function into a file called "myMochaSuite.js" which you can replace "describe" with when testing your api server. Be a wizard and put all your before/after stuff in this "suite". e.g.:
// tests/myMochaSuite.js
module.exports = (testDescription, testsCallBack) => {
describe(testDescription, () => {
const signUpThenLogIn = (credentials, testCallBack) => {
// The signUpThenLogIn function from above
}
before(async () => {
//before stuff like setting up the app and mongoose server.
})
beforeEach(async () => {
//beforeEach stuff clearing out the db
})
after(async () => {
//after stuff like shutting down the app and mongoose server.
})
// IMPORTANT: We pass signUpLogIn back through "testsCallBack" function.
testsCallBack(signUpThenLogIn)
})
}
// tests/my.api.test.js
// chai, supertest, etc, imports +
const myMochaSuite = require("./myMochaSuite")
// NB: signUpThenLogIn coming back into the tests.
myMochaSuite("my test description", signUpThenLogIn => {
it("just works baby", done => {
signUpThenLogIn(
{username: "wizard", password: "youSHALLpass"},
token => {
chai
.request(app)
.get("/apprentices/20")
// Using the incoming token passed when signUpThenLogIn callsback.
.set("Authorization", token)
.end((err, res) => {
res.body.name.equals("Apprentice 20, innit")
done()
})
}
)
})
})
Now you have a even more reusable suite "wrapper" for all your tests, leaving them uncluttered.
GraphQl full Example:
const adminLogin = async (agent) => {
const userAdmin = await User.findOne({rol:"admin"}).exec();
if(!userAdmin) return new Promise.reject('Admin not found')
return agent.post('/graphql').send({
query: ` mutation { ${loginQuery(userAdmin.email)} }`
})//.end((err, {body:{data}}) => {})
}
test("Login Admin", async (done) => {
const agent = request.agent(app);
await adminLogin(agent);
agent
.post("/graphql")
.send({query: `{ getGuests { ${GuestInput.join(' ')} } }`})
.set("Accept", "application/json")
.expect("Content-Type", /json/)
.expect(200)
.end((err, {body:{data}}) => {
if (err) return done(err);
expect(data).toBeInstanceOf(Object);
const {getGuests} = data;
expect(getGuests).toBeInstanceOf(Array);
getGuests.map(user => GuestInput.map(checkFields(user)))
done();
});
})

Resources