Testing Express / Passport middleware using Jasmine — passport.authenticate never completes

2k views Asked by At

I'm trying to unit test a simple piece of Express middleware, a cascading athenticator that checks first for a JWT token using a passport-jwt-strategy, and then if that fails, using a passport-openid-strategy. Each of the strategies is already well tested so what I am trying to test is their integration.

The module I am testing looks like this:

"use strict";

let passport = require('passport');
let Strategies = require('./strategies');
let setupDone = false;

// set up passport
let setup = function (app) {
    passport.serializeUser(function (user, done) {
        done(null, user);
    });

    passport.deserializeUser(function (obj, done) {
        done(null, obj);
    });

    passport.use('jwt', Strategies.jwt);
    passport.use('openid', Strategies.openId);
    app.use(passport.initialize());
    app.use(passport.session());
    setupDone = true;
};

let authenticate = function (req, res, next) {
    if (!setupDone) throw new Error('You must have run setup(app) before you can use the middleware');
    console.log(' cascadingAuthentication');
    // first try the token option
    passport.authenticate('jwt', function (jwterr, user, info) {
        console.log(' jwt auth', jwterr, user, info);
        if (jwterr || !user) {
            passport.authenticate('openid', function (oautherr, user, info) {
                if (oautherr || !user) {
                    return next(oautherr);
                } else {
                    next();
                }
            });
        } else {
            req.user = user;
            next();
        }
    });
};

module.exports = {
    setup: setup,
    authenticate: authenticate
}

My Jasmine test looks like this

"use strict";

let CascadingAuthentication = require('../../lib/middleware/cascadingAuthentication');
let TokenUtils = require('../support/tokenUtils');
let email = '[email protected]';

describe('cascadingAuthentication', function () {

    describe('when there is a token in the header', function () {
        let req;
        let res = {};
        let app = {
            use: function (used) { console.log('app.use called with', typeof used); }
        };

        beforeEach(function (done) {
            let token = TokenUtils.makeJWT(email);
            req = {
                app: app,
                header: {
                    Authorization: `Bearer ${token}`
                }
            }
            CascadingAuthentication.setup(app);
            CascadingAuthentication.authenticate(req, res, function () {
                done();
            });
        });

        it('populates req.user', function () {
            expect(req.user).toEqual(jasmine.any(Object));
        });
    });

});

The issue I have is that, when I run the test, I see the first console.log(' cascadingAuthentication') but I never see the second console.log('jwt auth', err, user, info). The code just dies inside passport.authenticate without ever calling the callback, without raising an error, or without providing any kind of feedback at all.

I'm running my tests via gulp using Jasmine.

My questions are: in order,

  1. Can you see anything obvious that I have done that I might have just missed?
  2. Is there anything else I ought to mock out in my req, res, or app that might make this test work?
  3. Is there any way to debug this interactively; stepping through the code under test as it runs, rather than just adding console.log statements (which seems a little 1980s to me).
2

There are 2 answers

0
Dave Sag On BEST ANSWER

Digging through passport's source I have worked out there were two problems with my code.

The first is that passport.authenticate returns a middleware function, it doesn't actually execute that function. So the solution was simply to call the returned function.

So my authenticate method now looks like:

let authenticate = function(req, res, next) {
  if (!setupDone) throw new Error('You must have run setup(app) before you can use the middleware');
  // first try the token option
  passport.authenticate('jwt', function(jwterr, user, info) {
    if (jwterr || !user) {
      passport.authenticate('openid', function(autherr, user, info) {
        if (autherr || !user) {
          return next(autherr);
        } else {
          next();
        }
      })(req, res, next);
    } else {
      req.user = user;
      next();
    }
  })(req, res, next);
};

(The above example is trimmed for use in the question)

The other issue was in my test I used header instead of headers in my mock req object, and also authorization ought to have had a lower case a.

With those two fixes the test now passes.

0
Steve Brush On

I fiddled with this for quite some time and eventually landed on the following setup (to test passport.authenticate('local', () => {})).

auth-router.js

const express = require('express');
const passport = require('passport');

const login = (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      next(err);
      return;
    }

    if (!user) {
      const error = new Error(info.message);
      error.status = 404;
      next(error);
      return;
    }

    // Add the found user record to the request to 
    // allow other middlewares to access it.
    req.user = user;
    next();
  })(req, res, next);
};

const router = express.Router();
router.post('/auth/login', login);

module.exports = {
  login,
  router
};

auth-router.spec.js

const passport = require('passport');

describe('login', () => {
  it('should login and add the user to the request object', (done) => {
    spyOn(passport, 'authenticate').and.callFake((strategy, callback) => {
      const err = null;
      const user = {};
      const info = {};
      callback(err, user, info);
      return (req, res, next) => {};
    });

    const auth = require('./auth'); // my middleware function
    const req = { body: {} };
    const res = {};
    const next = () => {
      expect(req.user).toBeDefined();
      done();
    };

    auth.login(req, res, next);
  });
});