2

My Goal

I'm trying to use supertest's agent function in a jest beforeEach() to login the user before each test, as I want each test to run under the assumption that the user is signed in. For authentication, I am using passport and passport-local.


This is what I tried (with parts cut out for brevity):

Test file:

import { agent, SuperAgentTest } from 'supertest';
import app from '../../src/app';

// create a `testRequest` variable to use in the tests
// that will be refreshed in between
let testRequest: SuperAgentTest;

const fakeUser = { email: 'john@john', username: 'john', password: 'john' };

beforeEach(async () => {
  // create new agent
  testRequest = agent(app);

  // register and login
  await testRequest.post('/register').send(fakeUser).expect(302);

  // other irrelevant stuff...
});

// protected route
describe('POST /campgrounds/new', () => {
  it('returns 200 OK', () => {
    return testRequest.get('/campgrounds/new');
  })
});

/register route:

router.post('/register', async (req, res) => {
  const { password, ...details } = req.body;
  try {
    // I am using passport-local-mongoose for this function-
    // it just registers the user
    const user = await User.register(new User(details), password);
    req.login(user, (err) => {
      // error handling and redirect
    });
  } catch (e) {
    // error handling
  }
})

This is my result

Instead of a 200 status, I get a 302 status, meaning I was redirected to the login page. To debug this, I created a test route called /current which will log the current user and session ID cookie. I then sent a GET request to this route in both the it and beforeEach function respectively.

Interestingly, they both logged the same session ID, but only the request in beforeEach had a user object attached to the request.

Lioness100
  • 8,260
  • 6
  • 18
  • 49
  • What is inside your req.login() method. Please post that also – Muhammad Saquib Shaikh May 03 '21 at 15:38
  • `req.login()` is a method automatically attached via passport. https://www.passportjs.org/docs/other-api/ – Lioness100 May 03 '21 at 16:31
  • Check your frontend is sending the credentials along with request. May be request to servers is not sent with cookies which contains the session. For reference look at this https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_include. Let me know if problem exists – Muhammad Saquib Shaikh May 04 '21 at 14:04
  • I already said that both requests share the same cookies – Lioness100 May 04 '21 at 16:01
  • Try replacing req.login code with this ```req.logIn(user, (err)=>{if(err) return next(err); return next()});``` – Muhammad Saquib Shaikh May 05 '21 at 02:06
  • There's no middleware after besides the error handler, and I'm handling the errors myself anyways, so I don't see why that would help – Lioness100 May 05 '21 at 12:35
  • Can you include the `/campgrounds/new` route? – Dan Levy May 07 '21 at 19:12
  • @DanLevy It basically just checks if `req.isAuthenticated()` is true (which just checks if `req.user` exists). If so, it redirects to the login page. If not, it renders an ejs form. – Lioness100 May 07 '21 at 21:39
  • I added more of an explainer to my answer below. Incl. how the plumbing works in passportjs. – Dan Levy May 07 '21 at 22:14

1 Answers1

3

#1 Ensure body parser correct order

Make sure you have this before any routes or auth-related things.

app.use(express.json())

#2 Check Passport Middleware Wire-up

Ensure you call app.use(passport.initialize()) & app.use(passport.session()) before any app.use('/', aRouter), router.get, router.post, etc:

// Set up session w/ specific config
app.use(session({
  secret: 'bquyqueajhbd',
  resave: true,
  saveUninitialized: true,
  store: new FileStore({path: '/tmp/session'})
}));
// Wire up the 
app.use(passport.initialize())
app.use(passport.session())

EDIT: Notes on req.user

Passport is designed to store the user ID in session.

Every request to the server must reload the user from the database. This is the job of the middleware passport.initialize() and passport.session().

The logic there will call passport.deserializeUser to lookup the user by ID - the same ID that was saved upon login into the session by passport.serializeUser.

passport.serializeUser(function(user, done) {
  done(null, user.id); // <-- Here's where the ID is saved to session.
});

passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user); // <-- Here is where the `req.user` get's it's value from.
  });
});

To debug this I'd focus on the passport.deserializeUser callback, add logs before and after the DB query.

(Note: it's been a few years since I taught this. Appologies if I'm not using the precise terms, etc.)

Dan Levy
  • 1,214
  • 11
  • 14
  • Thank you so much for the answer, I'll start another round of debugging. One question: is [body-parser](https://www.npmjs.com/package/body-parser) not needed anymore? I thought `bodyParser.json()` was better than `express.json()` – Lioness100 May 08 '21 at 02:20
  • 1
    IIRC, bodyParser was removed around Express v2-3 and re-added in the main package in recent versions. – Dan Levy May 08 '21 at 02:33
  • THANK YOU SO MUCH! After debugging `deserializeUser()` I found the problem! Suprise, surprise, it turns out the "other irrelevant stuff" in my `beforeEach()` function wasn't as irrelevant as I thought (it's a tale as old as time). I was deleting everything in my test database each time so that tests are individual, which obviously deleted the user from the database. idk how I didn't notice this! Enjoy your upvote, accepted answer, and bonus 100 rep for the help :) – Lioness100 May 08 '21 at 13:18
  • Awesome, glad to hear @Lioness100!! – Dan Levy May 10 '21 at 18:40