angular-fullstack / generator-angular-fullstack

Yeoman generator for an Angular app with an Express server
https://awk34.gitbook.io/generator-angular-fullstack
6.12k stars 1.24k forks source link

Question: API Testing with Passport #494

Open mousetree opened 10 years ago

mousetree commented 10 years ago

Hi, I'm not sure if this is the right place to post this question but I'm not having any luck elsewhere. I'm new to testing and I'm not having any luck on SO etc. as they lack the context of whats already provided by this generator.

Most of my routes on the API are protected. What I'm trying to do is write test specs that:

What I'm having trouble with, is how to write the tests such that:

Please help, i'm pulling my hair out!

mousetree commented 10 years ago

FYI, manually updated my project structure (including auth etc) to reflect that of https://github.com/DaftMonk/fullstack-demo (was using the previous version of the generator)

kingcody commented 10 years ago

Disclaimer: I'm very very new to unit testing.

That being said, this is what I would do to test server/auth/local/index.js: local.spec.js

var should = require('should');
var app = require('../../app');
var User = require('../../api/user/user.model');
var jwt = require('jsonwebtoken');
var config = require('../../config/environment');
var request = require('supertest');

describe('Local Auth API:', function() {

  // Clear users before testing
  before(function() {
    return User.remove().exec();
  });

  // Clear users after testing
  after(function() {
    return User.remove().exec();
  });

  describe('POST /auth/local', function() {
    var user;

    before(function(done) {
      user = new User({
        name: 'Fake User',
        email: 'test@test.com',
        password: 'password'
      });

      user.save(function(err) {
        if (err) return done(err);
        done();
      });
    });

    it('should respond with JWT when authenticated', function(done) {
      request(app)
        .post('/auth/local')
        .send({
          email: 'test@test.com',
          password: 'password'
        })
        .expect(200)
        .expect('Content-Type', /json/)
        .end(function(err, res) {
          if (err) return done(err);
          var token = res.body.token;
          jwt.verify(token, config.secrets.session, function(err, session) {
            if (err) return done(err);
            session._id.should.equal(user._id.toString());
            done();
          });
        });
    });

    it('should respond with 401 and a "message" when not authenticated', function(done) {
      request(app)
        .post('/auth/local')
        .send({
          email: 'test@test.com',
          password: 'bad-password'
        })
        .expect(401)
        .expect('Content-Type', /json/)
        .end(function(err, res) {
          res.body.message.should.be.type('string');
          done();
        });
    });
  });
});
kingcody commented 10 years ago

I know thats kind of heavy on the includes and unit test are supposed to test a very specific piece of an application, but I'm not sure how (without creating mocks) you would be able to test a local auth login strategy without them.

balthazar commented 10 years ago

I don't think that the authenticate test is what the OP ask here, but how to test the routes protected by the auth middleware

kingcody commented 10 years ago

I see, thanks @Apercu.

mousetree commented 10 years ago

Thanks @kingcody, but yeah as @Apercu said, need to test the protected routes. Any suggestions guys?

balthazar commented 10 years ago

I've also struggle quite a bit with this a few days ago and really interested in that, you could have a look to some of these links :

http://stackoverflow.com/questions/14001183/how-to-authenticate-supertest-requests-with-passport http://stackoverflow.com/questions/20640774/how-to-authenticate-supertest-requests-with-passport-facebook-strategy http://jaketrent.com/post/authenticated-supertest-tests/

I've try almost everything but I may have miss something

mousetree commented 10 years ago

Thanks @Apercu, I had a look at those yesterday but didn't manage to get them to work. I think i'll try again though. Essentially, as far as I can tell, in the 'before' i'll need to create the user, then authenticate it, and then hopefully superagent will remember that session so I can make requests on the protected routes...?

balthazar commented 10 years ago

Yeah that's what we should do I think. There's also a possibility of appending the auth result token to each of the query but I can't get that working either

kingcody commented 10 years ago

Perhaps this helps? server/api/user/user.spec.js

var app = require('../../app');
var User = require('./user.model');
var request = require('supertest');

describe('User API:', function() {
  var user;

  // Clear users before testing
  before(function(done) {
    User.remove(function() {
      user = new User({
        name: 'Fake User',
        email: 'test@test.com',
        password: 'password'
      });

      user.save(function(err) {
        if (err) return done(err);
        done();
      });
    });
  });

  // Clear users after testing
  after(function() {
    return User.remove().exec();
  });

  describe('GET /api/users/me', function() {
    var token;

    before(function(done) {
      request(app)
        .post('/auth/local')
        .send({
          email: 'test@test.com',
          password: 'password'
        })
        .expect(200)
        .expect('Content-Type', /json/)
        .end(function(err, res) {
          token = res.body.token;
          done();
        });
    });

    it('should respond with a user profile when authenticated', function(done) {
      request(app)
        .get('/api/users/me')
        .set('authorization', 'Bearer ' + token)
        .expect(200)
        .expect('Content-Type', /json/)
        .end(function(err, res) {
          res.body._id.should.equal(user._id.toString());
          done();
        });
    });

    it('should respond with a 401 when not authenticated', function(done) {
      request(app)
        .get('/api/users/me')
        .expect(401)
        .end(done);
    });
  });
});
mousetree commented 10 years ago

@kingcody: thanks, that's excellent. Seems to work perfectly! How would you go about refactoring that so that the user create, remove and authentication can be used in multiple specs? All my tests across the application will likely need to do those steps...

You're a star!

kingcody commented 10 years ago

Other than copy that setup to the other tests, I don't have a clean solution off the top of my head. If one comes to me, I'll be sure to fill you in. Glad I could help :)

lsiden commented 10 years ago

@kingcody, you should win a Nobel Prize for that!

Nevertheless, mine still doesn't work, but it may be a different problem.

  1) PUT /api/users/:id/contactInfo should return success if new password fields match:
     Uncaught TypeError: Cannot read property 'length' of undefined
      at model.UserSchema.path.validate.self (/home/lsiden/projects/cid/farmersmarket/app/server/api/user/user.model.js:69:17)
      at /home/lsiden/projects/cid/farmersmarket/app/node_modules/mongoose/lib/schematype.js:627:28
      at Array.forEach (native)
      at SchemaString.SchemaType.doValidate (/home/lsiden/projects/cid/farmersmarket/app/node_modules/mongoose/lib/schematype.js:614:19)
      at /home/lsiden/projects/cid/farmersmarket/app/node_modules/mongoose/lib/document.js:968:9
      at process._tickDomainCallback (node.js:463:13)

And the failing code:

// Validate empty email
UserSchema
  .path('email')
  .validate(function(email) {
    if (authTypes.indexOf(this.provider) !== -1) return true;
    return email.length;
  }, 'Email cannot be blank');

Hint: email is undefined.

I'm new to the MEAN stack. A friend got me to try out angular-fullstack. nodejs and Angular both make claims about ease of testing, but not in this particular framework!

mousetree commented 10 years ago

Hey guys, this is what I've been using:

I have a little auth helper for the tests that creates the users, authorizes them and then returns the users with their auth token.

auth.spec.helper.js

var supertest = require('supertest');
var app = require('../app');
var agent = supertest.agent(app);

var User = require('../api/user/user.model');
var async = require('async');
var _ = require('lodash');

// creates all users in mock and authenticates them
exports.initUsers = function(callback){
  var users = require('./auth.mock');
  User.remove({}, function(){
    async.mapSeries(users, function(user, cb) {
    initUser(user, cb);
  }, function(err, results){
      if (!err) {
        var x = {};
        _(results).each(function(result){ x[result.role] = result; });
        callback(x);
      }
    });
  });
};

// creates a single user and authenticates it
function initUser(fixture, cb){
  User.create(fixture, function(err, user){
    if (err){ cb(err); }
    fixture.id = user._id;
    agent
      .post('/auth/local')
      .send({username: fixture.username, password: fixture.password})
      .end(function(err, result){
        fixture.token = result.body.token;
        cb(null, fixture);
      });
  });
}

// deletes all users
exports.clearUsers = function(callback){
  User.remove({},function(){
    if (callback) { callback(); }
  });
};

auth.mock.js

module.exports = [{
    provider: 'local',
    name: 'Test User',
    username: 't1',
    email: 'test@test.com',
    password: 'test',
    role: 'user'
  },
  {
    provider: 'local',
    role: 'admin',
    name: 'Admin',
    username: 't2',
    email: 'admin@admin.com',
    password: 'admin'
  }
];

With the above code and the mocks, the helper would return an object containing two users, in the format users['user'] and users['admin']. Each of which would contain all the usual user attributes from the db as well an attribute token that contains the authorization token from passport.

Then, in all my tests that require auth, I include the helper as such:

kyc.spec.js

var Auth = require('../../auth/auth.spec.helper');
var KYC = require('./kyc.model');
var request = require('supertest');
var app = require('../../app');
describe ('KYC API',function(){

  var users; // stores the authorized users: user and admin

    // delete existing objects and initialise users
  before(function(done) {
    KYC.remove().exec().then(function() {
      Auth.initUsers(function(results){
        users = results;
        return done();
      });
    });
  });

  afterEach(function(done) { KYC.remove({}, done); });
  after(function(done) { Auth.clearUsers(done); });

  describe('GET /api/kyc', function() {

    it('should respond with data when authenticated', function(done) {
      request(app)
        .get('/api/kyc')
        .set('authorization', 'Bearer ' + users['user'].token)
        .expect(200)
        .expect('Content-Type', /json/)
        .end(function(err, res) {
          if (err) return done(err);
          res.body.should.be.instanceof(Array);
          return done();
        });
    });

    it('should respond with forbidden when not authenticated', function(done) {
      request(app)
        .get('/api/kyc')
        .expect(401)
        .end(function(err, res) {
          if (err) return done(err);
          return done();
        });
    });

  });

}); 

It seems to be working quite nicely, just need to include the helper and then call the initUsers method and save the users it returns. You then have users in the db that are authorized and can then use those tokens to make queries etc.

mousetree commented 10 years ago

The above is inspired by @kingcody 's solution above, just refactored into its own module so it can be re-used across multiple tests.

noamokman commented 9 years ago

I've done this a bit different,

var auth = require('../../auth/auth.service');

describe('/api/roles', function () {
  var user;
  var role;

  beforeEach(function (done) {
    clearModels()
      .then(function () {
        return createModels();
      })
      .spread(function (newRole, newUser) {
        role = newRole;
        user = newUser;
        done();
      })
      .catch(done);
  });

  it('should respond with JSON array', function (done) {
            request(app)
              .get('/api/roles')
              .set('Authorization', 'Bearer ' + auth.signToken(user._id))
              .expect(200)
              .expect('Content-Type', /json/)
              .end(function (err, res) {
                if (err) return done(err);
                res.body.should.be.an('array');
                res.body.length.should.equal(1);
                done();
              });
      });
});

This way, while testing the auth spaerately, I could save on the ,multiple auth requests for each test

lsiden commented 9 years ago

FWIW, I wrote a little wrapper https://github.com/lsiden/cid-farmersmarket/blob/master/server/api/helpers.service.js#L30 to help with this. On Tue Dec 23 2014 at 1:18:36 AM noamokman notifications@github.com wrote:

I've done this a bit different,

var auth = require('../../auth/auth.service');

describe('/api/roles', function () { var user; var role;

beforeEach(function (done) { clearModels() .then(function () { return createModels(); }) .spread(function (newRole, newUser) { role = newRole; user = newUser; done(); }) .catch(done); });

it('should respond with JSON array', function (done) { request(app) .get('/api/roles') .set('Authorization', 'Bearer ' + auth.signToken(user._id)) .expect(200) .expect('Content-Type', /json/) .end(function (err, res) { if (err) return done(err); res.body.should.be.an('array'); res.body.length.should.equal(1); done(); }); }); });

This way, while testing the auth spaerately, I could save on the ,multiple auth requests for each test

— Reply to this email directly or view it on GitHub https://github.com/DaftMonk/generator-angular-fullstack/issues/494#issuecomment-67925008 .

wmertens commented 9 years ago

What I don't understand is why the server authentication doesn't also read the token from the session cookie? Then you can browse the API in the browser without having to add Authorization headers. If there's a header, use that, if there is a cookie, use that.

The cookie is being added for most of the OAuth strategies anyway, so why not for all?

As you can see, not doing that results in a headaches and extra code on the API consumer side, without security improvements (AFAIK)?

kingcody commented 9 years ago

@chuyik you're correct regarding CSRF attacks and non http only cookies. However the token cookie is not http only as it is read by auth.service.js to log the user in automatically.

wmertens commented 9 years ago

@chuyik Not sure if that is a problem, since if you allow CORS/JSONP you presumably are happy with the token being used by other sites. Anyway, so maybe that should be configurable then...

wmertens commented 9 years ago

@chuyik I was under the impression that CORS prevents exactly this? Can you point me to a proof-of-concept implementation or explanation?

jeshuamaxey commented 9 years ago

I've adapted @chuyik's code to allow the test to specify a role for the user. Useful for testing endpoints which have different permissions

https://gist.github.com/jeshuamaxey/e88a21f802445bf05e18

Awk34 commented 9 years ago

@jeshuamaxey PR?

jeshuamaxey commented 9 years ago

I'm still ironing out some teething problems to do with tests running before the auth request returns a token. Once I have a fully working solution, I'll set up a PR :)

EDIT: I was running grunt serve and grunt watch:mochaTest simultaneously. They appear to have been interfering as they reloaded on save. Lesson learnt!

jeshuamaxey commented 9 years ago

FYI I've updated my gists with the implementation I've settled on for testing endpoints which require user authentification. They can be viewed here: https://gist.github.com/jeshuamaxey/e88a21f802445bf05e18

Next step is to develop this into a PR. With any luck I'll push something tonight, although will more likely be sometime mid next week.

lxibarra commented 8 years ago

Thanks to everyone contributing on this issue it was really helpful.