Testing using Mocha and Chai with Authentication
This is a small tutorial that will take you through testing using Mocha and Chai along with how to handle authentication inside your test case. This tutorial will take you from project creation straight to testing. To get started install using the instructions on the FeathersJS website:
$ npm install -g feathers-cli
$ mkdir test
$ cd test
$ feathers generate
After you type the generate command, it will ask a few questions to generate a base project depending on your needs:
? Project name test
? Description A simple featherjs test project
? What type of API are you making? REST
? CORS configuration Enabled for all domains
? What database do you primarily want to use? MongoDB
? What authentication providers would you like to support? local
You also need to install the following dependencies:
npm install --save-dev chai chai-http
When the project was generated, it created a user service. To make this a more realistic example, add another service using the following command:
feathers generate service
This brings up another list of options:
? What do you want to call your service? menu
? What type of service do you need? database
? For which database? MongoDB
? Does your service require users to be authenticated? yes
That will generate a menu folder under src/services and test/services. By saying yes to requiring users to authenticate, feathersjs generated some default authentication in src/services/menu/hooks/index.js.
exports.before = {
all: [
auth.verifyToken(),
auth.populateUser(),
auth.restrictToAuthenticated()
]
....
}
In addition to regular authentication, you can add role based authentication in src/services/menu/hooks/index.js by changing the previous code to the following:
exports.before = {
all: [
auth.verifyToken(),
auth.populateUser(),
auth.restrictToAuthenticated()
],
find: [
auth.restrictToRoles({
roles: ['admin']
})
],
get: [
auth.restrictToRoles({
roles: ['admin', 'user']
})
],
create: [
auth.restrictToRoles({
roles: ['admin']
})
],
update: [
auth.restrictToRoles({
roles: ['admin']
})
],
patch: [
auth.restrictToRoles({
roles: ['admin']
})
],
remove: [
auth.restrictToRoles({
roles: ['admin']
})
]
};
You can also add role based authentication to the user hook if you want to additional restrictions. Go ahead and update src/services/menu/menu-model.js to the following:
name: {
type: String,
required: true,
unique: true
},
price: {
type: Number,
required: true
},
categories: [{
type: String,
required: true
}],
createdAt: {
type: Date,
'default': Date.now
},
updatedAt: {
type: Date,
'default': Date.now
}
Now update the src/services/user/user-model.js to the following:
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
require: true
},
roles: [{
type: String,
required: true
}],
createdAt: {
type: Date,
'default': Date.now
},
updatedAt: {
type: Date,
'default': Date.now
}
Due to the change of the name field in the user-model.js you will also need to update the config/default.json file to the following:
"local": {
"usernameField": "username"
}
Finally change the test/menu/index.test.js file to the following:
'use strict';
const chai = require('chai');
const chaiHttp = require('chai-http');
const assert = require('assert');
const app = require('../../../src/app');
const Menu = app.service('menus');
const User = app.service('users');
const authentication = require('feathers-authentication/client');
const bodyParser = require('body-parser');
var token;
//config for app to do authentication
app
.use(bodyParser.json())
.use(bodyParser.urlencoded({ extended: true }))
.configure(authentication());
//use http plugin
chai.use(chaiHttp);
//use should
var should = chai.should();
describe('menu service', () => {
//setup
before((done) => {
//start the server
this.server = app.listen(3030);
//once listening do the following
this.server.once('listening', () => {
//create some mock menu items
Menu.create({
name: 'hamburger',
price: 7.99,
categories: ['lunch', 'burgers', 'dinner']
});
Menu.create({
name: 'spinach omlete',
price: 4.99,
categories: ['breakfast', 'omlete']
});
Menu.create({
name: 'steak',
price: 12.99,
categories: ['dinner', 'entree']
});
Menu.create({
name: 'reuben',
price: 6.99,
categories: ['lunch', 'sandwhich']
});
Menu.create({
name: 'soft drink',
price: 1.99,
categories: ['drinks', 'soda']
});
//create mock user
User.create({
'username': 'resposadmin',
'password': 'igzSwi7*Creif4V$',
'roles': ['admin']
}, () => {
//setup a request to get authentication token
chai.request(app)
//request to /auth/local
.post('/auth/local')
//set header
.set('Accept', 'application/json')
//send credentials
.send({
'username': 'resposadmin',
'password': 'igzSwi7*Creif4V$'
})
//when finished
.end((err, res) => {
//set token for auth in other requests
token = res.body.token;
done();
});
});
});
});
//teardown after tests
after((done) => {
//delete contents of menu in mongodb
Menu.remove(null, () => {
User.remove(null, () => {
//stop the server
this.server.close(function() {});
done();
});
});
});
it('registered the menus service', () => {
assert.ok(app.service('menus'));
});
it('should post the menuitem data', function(done) {
//setup a request
chai.request(app)
//request to /store
.post('/menus')
.set('Accept', 'application/json')
.set('Authorization', 'Bearer '.concat(token))
//attach data to request
.send({
name: 'shrimp fettuccine',
price: 12.99,
categories: 'dinner, pasta'
})
//when finished do the following
.end((err, res) => {
res.body.should.have.property('name');
res.body.name.should.equal('shrimp fettuccine');
res.body.should.have.property('price');
res.body.price.should.equal(12.99);
res.body.categories.should.be.an('array')
.to.include.members(['dinner, pasta']);
done();
});
});
});
This test file create some mock menu items and a user with admin rights in the before section. This section also has some logic to get a token. The token is then passed as a part of the request. With this code in place you can run it using either of the following commands:
mocha test/services/menu/index.test.js
npm run test
Testing Routes
The above example showed how to make a post request. Included here are other code snippets for making requests to different routes:
//test for put for /menuitem
it('should update the menuitem data', function(done) {
//test for put for /menuitem
chai.request(app)
//request to /menuitem
.put('/menus/' + res.body.data[2]._id)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer '.concat(token))
//attach data to request
.send({
name: 'steak sub',
price: 10.99,
categories: 'dinner, sandwhich'
}
)
//when finished do the following
.end(function(err, res) {
res.body.should.have.property('name');
res.body.name.should.equal('steak sub');
});
});
it('should delete the menu item data', function(done) {
//test for delete for /menuitem
chai.request(app)
//request to /menuitem
.delete('/menus/' + res.body.data[4]._id)
.set('Accept', 'application/json')
.set('Authorization', 'Bearer '.concat(token))
//when finished do the following
.end(function(err, res) {
//check returned json against expected value
res.body.name.should.be.a('string');
res.body.name.should.equal('soft drink');
});
});
});
//test for /menu get request
it('should get menu items', (done) => {
//setup a request
chai.request(app)
//request to /menu
.get('/menus')
.set('Accept', 'application/json')
.set('Authorization', 'Bearer '.concat(token))
//when finished do the following
.end((err, res) => {
//ensure menu items have specific properties
res.body.data.should.be.a('array');
res.body.data[0].should.have.property('name');
res.body.data[0].name.should.equal('hamburger');
});
});
The following code denotes that I have gotten the id used in MongoDB and passed that value into the route:
res.body.data[4]._id
You can view a demo of the files in the following link. These files are based on using FeathersJS generate file structure.