Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test a Node API that uses JWT Authentication (with User login to get token)

TL;DR - What is the way to test the resources in a Node API (Express) that uses JWT for Authentication with the token itself only granted to a username/password login?

I’m kinda new to testing and wanted to get some advice. The end goal is to have a fully tested API and then start learning how to get that hooked up to a Continuous Integration solution.

Technologies in use

  • I have written an API in Node using Express.
  • Mongo is the database.
  • Mongoose is used as the ODM.
  • jsonwebtoken package is used to create/verify tokens.
  • Passport is used to easily add User Authentication as Express middleware on the routes.

API Information

The API has various resources - the specifics of which aren’t important to this query but lets just pretend it’s the ubiquitous Todo app for simplicity’s sake.

Each individual resource saved in the database is associated with a single User.

The API uses JWT for authentication across the various resource endpoints. The token itself contains the unique User ID which is stored against the resource in a Mongo database. To get the token itself requires a user to first signup (which returns a token) and then login to get a new token.

Pretend code.

I’m going to simplify the code below and not make use of any environment configs, etc…

app.js

var express = require('express');
var app = express();
var mongoose = require('mongoose');
var bodyParser = require('body-parser');
var passport = require('passport');

mongoose.connect('mongodb://localhost/somedatabasename');

app.set('port', process.env.PORT || 3000);
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.use(passport.initialize());
// ... Passport JWT Strategy goes here - omitted for simplicity ...

var userRouter = require('./api/users/routes');
app.use('/users', userRouter);
var todoRouter = require('./api/todos/routes');
app.use('/todos', todoRouter);

app.listen(app.get('port'), function() {
  console.log('App now running on http://localhost:' + app.get('port'));
});

./api/todos/routes.js

var router = require('express').Router();
var controller = require('./controller');
var passport = require('passport');

router.route('/')
  .all(passport.authenticate('jwt', { session: false}))
  .get(controller.getAll)
  .post(controller.create);

router.route('/:id')
  .all(passport.authenticate('jwt', { session: false}))
  .get(controller.getOne)
  .put(controller.update)
  .delete(controller.delete);

module.exports = router;

./api/users/routes.js

var router = require('express').Router();
var controller = require('./controller');
var passport = require('passport');

router.route('/')
  // User signup
  .post(controller.create);

router.route('/me')
  // User Login
  .post(passport.authenticate('local', { session: false}), controller.login)
  // Get current user's data
  .get(passport.authenticate('jwt', { session: false}), controller.getOne)
  // Update current user's data
  .put(passport.authenticate('jwt', { session: false}), controller.update)
  // Delete current user
  .delete(passport.authenticate('jwt', { session: false}), controller.delete);

module.exports = router;

./api/users/model.js

var mongoose = require('mongoose');
var bcrypt = require('bcrypt');

var UserSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  }
});

// ... for simplicity imagine methods here to
// - hash passwords on a pre save hook using bcrypt
// - compare passwords using bcrypt when logging in

module.exports = mongoose.model('User', UserSchema);

./api/todos/model.js

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var momentSchema = new Schema({
  title: {
    type: String
  },

  // Bunch of other fields here...

  _user: {
    type: Schema.Types.ObjectId,
    ref: 'User'
  }
});

module.exports = mongoose.model('Moment', momentSchema);

I have omitted some of the example code to keep it clean and simple.

For example, the User's controller would include the Models and its functions would:

  • controller.create - signup for new users (returns token)
  • controller.login - after username/password combination confirmed by Passport Local then return a valid token
  • controller.getOne - based on User ID retrieved from JWT token, return the user's data from Mongo using Mongoose.
  • controller.update - Update the User's data in Mongo using Mongoose
  • controller.delete - Delete the User's data in Mongo using Mongoose

The Todo's controllers would do something similar - just interacting with Mongo data via Mongoose but the queries would always include the User ID to associate the specific (for example) Todo item with the authenticated User (authenticated via JWT).

Testing Conundrum

How would I go about testing something like this using a combination of Mocha, Chai and SuperTest?

Would I:

  • create a test database within Mongo and have the connection string be different in the tests? This would mean saving actual data stored in the database for testing.
  • Mock the data somehow and not use a test database at all? But then how is user saving / login handled to retrieve the token?

How would the testing work locally when developing versus when you do a deployment using some CI tool (something I have yet to even get to in my studies)?

Any assistance would be much appreciated and I hope I've given enough info with the dummy data/code above :/

like image 515
nodenoob Avatar asked May 17 '16 19:05

nodenoob


People also ask

How do I get an API with JWT token?

To authenticate a user, a client application must send a JSON Web Token (JWT) in the authorization header of the HTTP request to your backend API. API Gateway validates the token on behalf of your API, so you don't have to add any code in your API to process the authentication.


1 Answers

During testing, you would normally mock your mongo DB (something like mongo-mock. This way, you do not need an actual database running to run your tests (you are not testing the database, but your code).

During testing, you would replace the mongodb with mongo-mock and then run your test. To get your token, you would need to post to your /me URL with valid mocked credential, that endpoint would return the token, which you would then use on your next call to test your other endpoint.

On the token side of things, I usually check it at the beginning of the request before entering the other endpoints. (I have not used passport but the idea is):

app.use(validate_jwt_middleware);
app.use('/users', userRouter);

This way, if the token is invalid, it's invalid for the whole site, not only your section.

Also, I'm not using SuperTest, but chai-http, so I can't help you with your specifics.

Hope this helps,

like image 189
Danosaure Avatar answered Oct 15 '22 15:10

Danosaure