75

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?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Gal Ben-Haim
  • 17,433
  • 22
  • 78
  • 131

8 Answers8

65

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();
        }
    };
};
hopper
  • 13,060
  • 7
  • 49
  • 53
Andy
  • 983
  • 7
  • 13
  • 15
    If you put your app.js into ```request.agent(app);``` it works without a running server. Cool stuff. – shredding Dec 15 '14 at 14:41
  • 1
    This just got me out of a 3 days hell of stubbing, mocking, require cache cleaning and soul crushing attempts... Cheers! – neric Oct 14 '18 at 15:42
  • More examples can be found here: https://github.com/visionmedia/superagent/blob/master/test/node/agency.js – Taoufik Mohdit Feb 06 '22 at 18:45
  • takes 900 ms to call /login once, the whole test suit with 200 tests is now taking 15s per run – PirateApp Apr 30 '22 at 03:52
55

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.

zemirco
  • 16,171
  • 8
  • 62
  • 96
  • 4
    with this method I need to have a test server running. is it possible to use it with Supertest's server ? I'm using session cookies (with Passport) and it doesn't work, I look at the response from user1.post and the cookie doesn't contain the user information – Gal Ben-Haim Dec 22 '12 at 13:51
  • 2
    you don't need a test server. You can use your normal express app.js. Did you have a look at the [example](https://github.com/visionmedia/superagent/blob/master/test/node/agency.js)? If you want to keep the tests in a separate file put `require(../app.js)` into the header to start your app. – zemirco Dec 22 '12 at 14:07
  • 3
    I got it to work, but only if I kill the development server that already runs. with supertest I don't have to do that. any ideas how to make it play nicely with superagent ? maybe listen to a different port for test environment ? – Gal Ben-Haim Dec 22 '12 at 14:35
  • 1
    So how do I make another request and use that user1 session in other `it("should create an object by this user1")` tests? – chovy Jul 17 '14 at 06:18
  • You could bind the port number that your server runs at to an environment variable and set the port number when you run the tests to a port number not being used by your server. – mikeyGlitz Sep 13 '16 at 01:21
  • BTW A bit of a gotcha that blew up 2 hours today: If you are using local strategy from the typical passport example be sure and set `Content-Type` to `application/x-www-form-urlencoded` using the supertest `set` command, or passport+express will quietly ignore your request to login. The quiet part is especially annoying. – Paul S Nov 23 '16 at 08:56
30

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();        
  });
grub
  • 326
  • 3
  • 5
  • The second call to request never fires. That is, the .end handler never gets reached. – juanpaco Aug 05 '13 at 11:11
  • 4
    This works just fine if the second request is placed inside the first end callback. – JayPea Aug 26 '13 at 17:13
  • Sorry for the downvote, but `request.agent(app)`, as per Andy's answer, is much more elegant than manually setting cookies. – Kevin C. Mar 03 '14 at 23:10
  • my session api doesn't set a cookie. it returns a user object which client stores. – chovy Jul 17 '14 at 07:01
  • 1
    i set a variable in outside the route and assign it inside and use it to auth `.expect(res => { cookie = res.headers["set-cookie"]; })` – Nenoj Mar 13 '19 at 04:48
11

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();
    });
  });
});
Kevin C.
  • 2,499
  • 1
  • 27
  • 41
  • 3
    Should probably be `expect(res.status)` rather than `req.status`. –  Dec 09 '15 at 21:50
  • The best answer. – paqash Sep 12 '18 at 11:53
  • This worked for me, using a passport LocalStrategy for authentication. In my case two more changes were needed. First, I had to change `afterEach()` so that it dropped all collections except the users. Second, I had to call jest with the `--runInBand` option, which makes tests run in the order listed. – Raffi Jul 06 '21 at 11:18
  • My code: `var request = require("supertest"), app = require("../app"), agent = request.agent(app); describe("Notifications", () => { const username = "u", pwd = "p"; let user; it("logs in", async () { user = new User({username}); user.setPassword(pwd); await user.save(); agent.post('/login').send({username, pwd}).expect(302); }); it('shows notification', async () => { const msg = "msg"; const n = new Notification({user, msg}); await n.save(); agent.get("/").expect(200).end(function(err,res) { if(err){ return err; } expect(res.text).toMatch(msg); }); });` – Raffi Jul 06 '21 at 11:29
8

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!

Dan K.K.
  • 5,915
  • 2
  • 28
  • 34
3

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.

juanpaco
  • 6,303
  • 2
  • 29
  • 22
  • Alternatively you could just test your request handler directly, passing it some dummy req and res objects. That, of course, wouldn't test your routing though. – juanpaco Aug 05 '13 at 12:50
0

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.

Timothy Bushell
  • 170
  • 1
  • 7
0

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();
    });
})
ValRob
  • 2,584
  • 7
  • 32
  • 40