How to authenticate Supertest requests with Passport? - node.js

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();
});
})

Related

How to test requests that require the user to be authenticated in Express

How can you test requests that require the user to be authenticated? I'm using local passport.js in my express app. I'm testing using Jest and Supertest. I've looked up countless posts, I tried supertest-session and that doesn't seem to work either. All of these posts are from almost 10 years ago so I'm not sure if they're still valid.
Here is the last thing that I've tried:
const request = require('supertest');
const app = require('../app');
const { pool } = require('../dbConfig');
let testAccount = { email: 'loginTestUser#gmail.com', password: '123456' }
const loginUrl = '/users/login';
const createRoomUrl = '/rooms/create'
const successfulRoomCreateUrl = '/users/dashboard';
const successfulLoginUrl = '/users/dashboard';
const failedRoomCreateUrl = '/';
afterAll(async () => {
pool.end();
});
describe('Room CRUD: Create | POST /rooms/create', () => {
describe('Given the user is Authenticated', () => {
let token;
beforeEach( async () => {
const response = await
request(app).post(loginUrl).type('form').send(testAccount);
token = { access_token: response.body.token }
});
test(`should get statusCode 200 if user is logged in`, async () => {
const createRoomResponse = await request(app).get(createRoomUrl).query(token);
// 302 since the user doesn't stay logged in
expect(createRoomResponse.statusCode).toEqual(200);
});
});
});
Here is what I tried with supertest-session and it also doesn't work:
const session = require('supertest-session');
const myApp = require('../app');
let testAccount = { email: 'loginTestUser#gmail.com', password: '123456' }
var testSession = null;
beforeEach(function () {
testSession = session(myApp);
});
it('should fail accessing a restricted page', function (done) {
testSession.get('/rooms/create')
.expect(302)
.end(done)
});
it('should sign in', function (done) {
testSession.post('/users/login')
.send(testAccount)
.expect(302) // get redirected to dashboard
.end(done);
});
describe('after authenticating session', function () {
var authenticatedSession;
beforeEach(function (done) {
testSession.post('/users/login')
.send(testAccount)
.expect(302) // get redirected to /users/dashboard
.end(function (err) {
if (err) return done(err);
authenticatedSession = testSession;
return done();
});
});
it('should get a restricted page', function (done) {
authenticatedSession.get('/rooms/create')
.expect(200) // <------ still get a 302 (redirect if user !logged
.end(done)
});
});
Turns out all I needed to do was append .type('form') like so:
beforeEach(function (done) {
testSession.post('/users/login').type('form')
.send(testAccount)
.expect(302) // get redirected to /users/dashboard
.end(function (err) {
if (err) return done(err);
authenticatedSession = testSession;
return done();
});
});

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

Chai-http: cannot set value of nodes ctx.state

I am creating an API with NodeJS and KOA. For testing, I use chai (chai-http) and mocha.
The problem arises when I use const { username } = ctx.state.user in my controllers to get the username of the user that sent the request. When accessing them with my application (with Flutter) or with Postman, it works, but when running the tests with mocha, i get the error TypeError: Cannot destructure property 'username' of 'undefined' or 'null'. When debugging the code, i found that ctx has a key for state but the value for that key was empty. I tried the methods .set(...) and .send(...) but they only modified values inside ctx.request.header and ctx.request.body.
So my question is: is it possible to set a value for ctx.state with chai and if so, how? I would like to put in something like {user: {username: 'chai'}}.
Here are the 2 main parts, the controller section to be tested and the test method:
async function bar(ctx, next) {
const { username } = ctx.state.user;
const { value } = ctx.request.body;
// do something with value and username
ctx.status = 200;
}
it("With correct gameKey: should return the rating of the game", done => {
chai
.request('http://localhost:3000')
.post('/foo/bar')
.set('content-type', 'application/json')
.send({value: 3});
.end((err, res) => {
// do some tests
done();
});
});
Here is the whole code from the server index and the test file:
const Koa = require('koa');
const Jwt = require('koa-jwt');
const Router = require('koa-router');
const Cors = require('#koa/cors');
const BodyParser = require('koa-bodyparser');
const Helmet = require('koa-helmet');
const app = new Koa();
const router = new Router();
router.post('/foo/bar', bar);
async function bar(ctx, next) {
const { username } = ctx.state.user;
const { value } = ctx.request.body;
// do something with value and username
ctx.status = 200;
}
app.use(Helmet());
app.use(Cors());
app.use(BodyParser({
enableTypes: ['json'],
strict: true,
onerror(err, ctx) {
ctx.throw('Request body could not be parsed', 422);
},
}));
app.use(Jwt({ secret: process.env.SECRET }).unless({
path: [
// Whitelist routes that don't require authentication
/^\/auth/,
],
}));
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000, () => console.log(`API server started on localhost:3000`));
const chai = require("chai");
const chaiHttp = require("chai-http");
chai.use(chaiHttp);
const expect = require('chai').expect;
const userCredentials = {
username: 'chai',
password: 'chai'
}
describe("Route: games/", () => {
before(() => {
chai
.request('http://localhost:3000')
.post('/auth')
.set('content-type', 'application/json')
.send(userCredentials)
.end((err, res) => {
expect(res.status).to.be.eql(200);
});
});
describe("Sub-route: GET /", () => {
describe("Sub-route: PUT /:gameKey/rating", () => {
it("With correct gameKey: should return the rating of the game", done => {
chai
.request('http://localhost:3000')
.post('/foo/bar')
.set('content-type', 'application/json')
.send({value: 3});
.end((err, res) => {
expect(res.status).to.be.eql(200);
done();
});
});
});
});
});
According to KOA documentation ctx.state The recommended namespace for passing information through middleware and to your frontend views.
app.use(function * (next) {
var ctx = this
ctx.request
ctx.response
ctx.body = 'hello'
ctx.state.user = yield User.find(id).fetch()
next()
}
So i think you can set ctx.state in middleware function which you can use in next request handler.
I figured it out thanks to the comment of Sandeep Patel. I had to use the middleware, so i saved the token retreived with the request in the before() method and added it to the other requests with .set("Authorization", "Bearer " + token).
The working test then looks like this:
const chai = require("chai");
const chaiHttp = require("chai-http");
chai.use(chaiHttp);
const expect = require('chai').expect;
const userCredentials = {
username: 'chai',
password: 'chai'
}
describe("Route: games/", () => {
var token;
before(() => {
chai
.request('http://localhost:3000')
.post('/auth')
.set('content-type', 'application/json')
.send(userCredentials)
.end((err, res) => {
expect(res.status).to.be.eql(200);
token = res.body.token; // Added this
});
});
describe("Sub-route: GET /", () => {
describe("Sub-route: PUT /:gameKey/rating", () => {
it("With correct gameKey: should return the rating of the game", done => {
chai
.request('http://localhost:3000')
.post('/foo/bar')
.set("Authorization", "Bearer " + token) // Added this
.set('content-type', 'application/json')
.send({value: 3});
.end((err, res) => {
expect(res.status).to.be.eql(200);
done();
});
});
});
});
});

Jest Memory Leak testing Express Middleware

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.

Set authentication token to chai-Http

I have created some test cases in node express framework which return successfully. However there are some API calls which need to have authenticated before I can make them. I can call the API that requires authentication in reactJs via ajax and It give a success response, however when I set the same header in chai-Http, it fails with invalid token.
var chai = require('chai');
var should = chai.should();
var chaiHttp = require('chai-http');
chai.use(chaiHttp);
var server = require('../app');
describe('routes : index', function() {
var token = '';
beforeEach(function(done) {
done();
});
before(function(done) {
chai.request(server)
.post('/doSignin')
.send({uname:'ruzan#test.com',password:'123123'})
.end(function(err, res) {
res.redirects.length.should.equal(0);
res.status.should.equal(200);
token = res.body.user.token;
res.type.should.equal('application/json');
done();
});
});
afterEach(function(done) {
done();
});
describe('Test FolderController', function() {
it('should list all the folders', function(done) {
console.log(token); // here it print the same token
chai.request(server)
.get('/notes/get-notes')
.set('prg-header' , token)
.end(function(err, res) {
// console.log(res);
res.status.should.equal(200);
done();
});
});
});
});
ReactJs:
$.ajax({
url: '/notes/get-notes',
method: "GET",
dataType: "JSON",
headers: { 'prg-header':loggedUser.token }
}).done( function (data, text) {
console.log(data);
if(data.status.code == 200){
// it come here the data
}
}.bind(this));

Resources