Quick Introduction: Mocha, Chai and Chai-http Test Express API Auth Endpoints
Irrespective of the programming language or framework used, Obeying the Testing Goat is essential and leads to reduced error-prone programs or applications. Let’s put Mocha, Chai and Chai-HTTP to use and see how we can test an Express API endpoints behind authentication.
As much as testing is generally not fun to write, we all appreciate the benefits. Fortunately, I’m not here to tell you what goodness testing comes with, and how it can improve your code base if you have good test coverage.
In this article, let’s explore how to handle testing API endpoints which are protected. In our setup, we’ll be using JWTs, therefore, a way to maintain the JWT in subsequent tests without refreshing is important, in order to pass a flow.
Our stack for this article are:
- Express (duh)
- Mocha (
npm install --save-dev mocha
) - Chai and Chai-HTTP (
npm install --save-dev chai chai-http
)
Of course, this article assumes you have a bit of experience using at least, Javascript of which Express is made of.
If you’ve played with Express before, then that’s fine. In any, I think developers interested in testing know their way around.
So let’s get in now that we have our needed packages installed.
What to Test?
In short, we wanna test these:
/GET login
and/POST login
/GET register
and/POST register
/POST register
,/POST login
and then/GET user
(a protected endpoint)
I won’t go over the steps for 1. and 2. as the third point will take us through all that, and throw in the accessing a protected route, namely, /GET user
, which returns a User info.
Therefore, our /GET register
and /POST register
endpoints can like this (for brevity, some code part will be cut short)
// entire api endpoint url looks like this eventually: // localhost:3000/api/v1/auth/register router.get('/register', function(req, res) { res.json({ 'state': true, 'msg': 'Register endpoint', 'data': { 'username': 'username', 'email': 'email', 'password': 'password', 'fullName': 'full name' } }); }); router.post('/register', function(req, res) { // check if all required were submitted if (req.body.username && req.body.email && req.body.password) { let newUser = new User(req.body); //this is safe, because of defined schema behind the scenes newUser.hash_password = bcrypt.hashSync(req.body.password, 10); newUser.save(function(err, user) { if (err) { if (err && err.code === 11000) { // this is Duplicate key error code from MongoDB return res.status(500).json({ 'state': false, 'msg': 'User with this email address exist.' }) } return res.status(400).json({ 'state': false, 'msg': err.message }); } else { verification_token = crypto.randomBytes(20).toString('hex'); // Do a verification_token 'distin' here. /** * Reads an html file, and sends it as email, replacing values in it with params */ utils.readHTMLFile('./public/email/verify-email.html', function(err, html) { // handlebars replaces parts in html for us /* let template = handlebars.compile(html); let replacements = { .... }; let mailOptions = { ... }; */ // ignore sending mail if running in test mode if (req.app.get('env') === 'test') { // Don't send mail console.log('Sending no mail, in test mode'); } else { // Send actual email sendAnEmail(mailOptions); } // Bingo! res.status(201).json({ 'state': true, 'msg': 'Registration Successful. Kindly check your email for further instructions.' }); }) } }); } else { res.json({ 'state': false, 'msg': 'Please include all required credentials, namely, Email, Username and Password' }) } });
Our /GET login
and /POST login
can look on the other hand, like this:
// localhost:3000/api/v1/auth/login router.get('/login', function(req, res) { res.json({ 'state': true, 'msg': 'Login page', 'data': { 'email_or_username': 'username', 'password': 'password' } }); }); router.post('/login', function(req, res) { console.log(req.body); if (req.body.email_or_username && req.body.password) { let email; let username; let user_info = req.body.email_or_username.trim().toLowerCase(); // Check if email or username console.log(validator.isEmail(user_info)); if (validator.isEmail(user_info)) { email = user_info; console.log('assigned as email'); } else { username = user_info; console.log('assigned as username'); } let password = req.body.password // ensure these values were actually sent // they're compulsory User.findOne({ $or: [ // login with either 'username' or 'email' { email: email }, { username: username } ] }, function(err, user) { if (err) throw err; if (!user) { res.status(401).json({ 'state': false, 'msg': 'Authentication failed. We have no user with such credentials.' }); } else if (user) { if (!user.comparePassword(password)) { res.status(401).json({ 'state': false, 'msg': 'Authentication failed. Wrong email or password. Try again.' }); } else { user.last_login = Date.now(); user.save(); return res.json({ 'state': true, 'user': { 'fullName': user.fullName, 'email': user.email, 'username': user.username }, 'token': "JWT" + " " + jwt.sign({ 'email': user.email, 'username': user.username, '_id': user._id }, config.secret, { expiresIn: 60 * 60 * 24 }) }) } } }) } else { res.json({ 'state': false, 'msg': "Missing credentials. Provide 'email' or 'username' AND 'password'" }) } });
Nothing interesting happening in the above snippet though. We’re logging in!
Now let’s test!
Testing, Shall we?
First of all, add something small to your package.json
file indicating a command to run the test.
{ "name": "appname", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/www", "test": "mocha --timeout 3000 --recursive" }, "dependencies": { ... }, "devDependencies": { ... } }
With the above in our package.json
file, we can simply do, npm test
to trigger the running of the test. But we even have no test file yet.
Let’s do that.
process.env.NODE_ENV = 'test'; let mongoose = require('mongoose'); let User = require('../models/userModel.js'); let chai = require('chai'); let chaiHttp = require('chai-http'); let server = require('../bin/www'); let should = chai.should(); let expect = chai.expect; chai.use(chaiHttp); let login_details = { 'email_or_username': 'email@email.com', 'password': '123@abc' } let register_details = { 'fullName': 'Rexford', 'email': 'email@email.com', 'username': 'username', 'password': '123@abc' }; /** * Test the following in on scoop: * - Create an account, login with details, and check if token comes */ describe('Create Account, Login and Check Token', () => { beforeEach((done) => { // Reset user mode before each test User.remove({}, (err) => { console.log(err); done(); }) }); describe('/POST Register', () => { it('it should Register, Login, and check token', (done) => { chai.request(server) .post('/api/v1/auth/register') .send(register_details) // this is like sending $http.post or this.http.post in Angular .end((err, res) => { // when we get a response from the endpoint // in other words, // the res object should have a status of 201 res.should.have.status(201); // the property, res.body.state, we expect it to be true. expect(res.body.state).to.be.true; // follow up with login chai.request(server) .post('/api/v1/auth/login') .send(login_details) .end((err, res) => { console.log('this was run the login part'); res.should.have.status(200); expect(res.body.state).to.be.true; res.body.should.have.property('token'); let token = res.body.token; // follow up with requesting user protected page chai.request(server) .get('/api/v1/account/user') // we set the auth header with our token .set('Authorization', token) .end((err, res) => { res.should.have.status(200); expect(res.body.state).to.be.true; res.body.data.should.be.an('object'); done(); // Don't forget the done callback to indicate we're done! }) }) }) }) }) })
Now, go back and read through the code, isn’t it just beautiful, how writing tests with Chai mean we get to use common English expressions.
The steps involved in the above snippet is this:
- To make request to protected pages, we need to have an auth token
- To have an auth token, we need to login
- To be able to log in, we need to have an account.
- Therefore, we create an account, then login, then extracting the auth token, we make a request to the protected page/endpoint.
- We signal the end of our tests with the
done()
callback. Indeed, we’re done!
Nesting your a request, yet in the .end( .. )
chained sections means we get access to the either err
or res
objects from the previous request. With that, the rest is history.
We get to manipulate our res
object just like we do in usual Express. Nothing special.
Checkout the Chai docs to see more available expressions: http://devdocs.io/chai/api/bdd/index
When we run the above, if all goes well, we can see something like this
> khophiserver@0.0.0 test /home/khophi/Developments/Express/KhophiServer > mocha --timeout 3000 --recursive Using testDB Running mode: test Listening on: http://localhost:3011 Create Account, Login and Check Token /POST Register null Sending no mail, in test mode POST /api/v1/auth/register 201 118.103 ms - 97 true assigned as email POST /api/v1/auth/login 200 91.769 ms - 343 this was run the login part GET /api/v1/account/user 200 6.253 ms - 366 ✓ it should Register, Login, and check token (244ms) User Auth Approaches /GET auth login null GET /api/v1/auth/login 200 0.633 ms - 95 ✓ it should display login page /POST auth login null POST /api/v1/auth/login 200 0.682 ms - 89 ✓ it should throw error with invalid credentials /GET auth register null GET /api/v1/auth/register 200 0.450 ms - 132 ✓ it should get register endpoint /POST auth register successfully null Sending no mail, in test mode POST /api/v1/auth/register 201 79.321 ms - 97 ✓ it should register new user (81ms) /POST auth register fail null POST /api/v1/auth/register 200 0.406 ms - 101 ✓ it should fail to register a new user 6 passing (371ms)
All our tests passed. Don’t say because I say it passed, it passed for you too. Play with the examples above. Create your own tests, as I have in my test results above.
You can decide to test different scenarios of your application, thus, if something doesn’t go right in future updates and code increments you can pick them up easily.
Writing test.should.be.fun and using Chai and Chai-http makes the experience easier.
Conclusion
Lemme know in the comments if you don’t understand any parts. Will be happy to help. Otherwise
I hope to write more tests for my upcoming Progressive Web App, which will live at khophi.com. It is from the codebase of the project which the above snippets were taken from.
Hope to you see you in the next one.