Hi, this is part 2 in this series. To read part 1 go here.

Tests for the User APIs

So we’ll start with writing some tests. It’s importance to add a few tests to your app. These will be super helpful moving forward. We will add a few basic tests so we can set it up and move forward quickly.

First just create the file tests/users.js . This file should contain all tests for users. Please not that currently we don’t have much to test. The only thing we have is creation etc.

Please check the test file here. It should be self explanatory.

Requirements — What are we building?!

Well before we start building something it’s always better to have some concrete idea about what we want to build. Right now, I’m just going to write it down, but in real-life this job should ideally be done in conjunction with the Product/Marketing/Sales etc. teams so ideas about the actual requirements can be assessed.

User Stories

User flow/stores are an interesting method to do this. We basically assess what we want the user to do. Please also note, this is an API. It’s not a front-end app. So the user stories we write will be API consumer centric. These stories can also be used to write tests. Without further ado, these are the requirements for our API :

User should be able to login via a username and password and receive an access token. User should be able to add an expense/income of any amount. User should be able to add categories to the entries. User should be able to tag any entry with multiple tags. User should be able to see an account wise and overall view of his or her expenses for a specified period of time. User should be able to get entries for a specified time period.

Now as you can see, the first task is already done. We’ll move on to the rest. Please note that these are just requirements for now. Later we may have more or want more changes. For now, we’ll just follow this.

Story 1: Adding entries

Now that we have the tests setup, let’s move on to the actual expense tracking. Let me explain the database structure I want to follow. Please note that this is the structure which I want. There can be many more structures. I’m just using this one.

The migration

So we’ll have an entries (income and expense) table. We create a new migration with the following code at db/migrations/<timestamp>_entries.js .

To create the migrations file, run the following command knex migrate:make entries .

Add the following to the migration file created.

exports.up = function(knex, Promise) {

return knex.schema.createTable('entries', (t) => {

t.increments(); // Name of this entry, like "Got milk for cat"

//P.S. don't get milk for cat. It's not good for cat

t.string('name'); t.decimal('amount', 2); t.bigInteger('user_id')

.references('id')

.inTable('users'); t.timestamps(true, true);

});

}; exports.down = function(knex, Promise) {

return knex.schema.dropTable('entries');

};

Please note how I’m referencing the user_id key to create a relation with the users table.

Now multiple times it happens that in a database we want to use snake_case but in our code we use camelCase . This is can be an issue for you (like it is for me). Luckily, objection provides an easy way to remedy this problem. Basically we use knexSnakeCaseMappers from objection, so that this is done for us automatically.

Edit the db/index.js file as follows:

const { Model, knexSnakeCaseMappers } = require('objection');

const Knex = require('knex'); const dbConfig = require('../config/db'); // Initialize knex.

const knex = Knex({

client: dbConfig.client,

useNullAsDefault: true,

connection: dbConfig.connection,

...knexSnakeCaseMappers()

}); // Give the knex object to objection.

Model.knex(knex); module.exports = {

knex,

Model

}

Look here for details on various ways to do this. We add it directly to knex, instead of asking objection to do it. Essentially it uses the postProcessResponse hook in knex to rewrite the response by replacing snake_case with camelCase .

The model

Now that we are done with that, let’s create a model for this. We create the file db/models/Entries.js and add the following to it

const { Model } = require('../index'); class Entries extends Model {

static get tableName() {

return 'entries';

} static get relationMappings() {

return {

// Create a mapping with the user model

user: {

relation: Model.BelongsToOneRelation,

modelClass: `${__dirname}/Users.js`,

// Where the two are joined

join: {

from: 'entries.userId',

to: 'users.id'

}

}

};

}

} module.exports = Entries;

Note how we use the relationsMappings static function to connect the two. This helps us with eager loading. What is eager loading you ask? Well you can get spoilers here, or you can wait for the big reveal, coming up in a little bit (it’ll be worth the hype, I promise!).

Now we also must add this mapping to the Users model.

Edit the file db/models/Users.js as follows.

const { Model } = require('../index'); class Users extends Model {

static get tableName() {

return 'users';

} static get relationMappings() {

let Entries = require('./Entries');

return {

// Create a mapping with the user model

entries: {

relation: Model.HasManyRelation,

modelClass: Entries,

// Where the two are joined

join: {

from: 'users.id',

to: 'entries.userId'

}

}

};

}

} module.exports = Users;

Note how we’ve added the modelClass a bit differently. Either case is fine. You can read the docs of objection.js to understand better how each way affects the app.

The tests

Now earlier, we wrote the tests after writing the APIs. This is a good enough approach, however, the Test Driven Development (TDD) approach, helps us first define what our APIs must look like, and then create the APIs. The tests help us validate the APIs and also help us think critically about the APIs. So let’s get to it.

Let’s understand how the user can accomplish this task.

User will create an account. User will login and get an auth token. User will hit the API to add the entry.

So we create a file tests/entries/addEntry.js . The aim of this file is to test if we can successfully add an entry. So go ahead and create this file and add the following

const Users = require('../../db/models/Users');

const Entries = require('../../db/models/Entries'); //Require the dev-dependencies

const chai = require('chai');

const chaiHttp = require('chai-http');

const server = require('../../bin/www');

const expect = chai.expect; chai.use(chaiHttp); describe('Entries', () => {

before(async () => {

// Empty out the entries

await Entries.query().delete();

// Empty out users

await Users.query().delete();

}); describe('Adding an entry', async () => {

it('should add an entry', async () => {



}); });

});

Now we need to create a user. While we can use the same thing we used in the earlier test, it’s better to create a helper for such things. So go ahead and create a new file tests/testHelpers/userHelper.js and add the following.



* Hits the API to create new user

*

*

*

*

*/

const addUser = async (

server,

username = "pritoj",

email = "

password = "12345") => { /*** Hits the API to create new user @param {server} server Chai server instance @param {string} username @param {string} email @param {string} password*/const addUser = async (server,username = "pritoj",email = " pritojs@gmail.com ",password = "12345") => { return await server

.post('/users')

.send({

username,

email,

password

}); }

* Hits the API to login

*

*

*

*/

const loginUser = async (

server,

email,

password

) => {

return await server

.post('/users/token')

.send({

email,

password

});

} /*** Hits the API to login @param {server} server Chai server instance @param {string} email @param {string} password*/const loginUser = async (server,email,password) => {return await server.post('/users/token').send({email,password}); module.exports = {

addUser,

loginUser

}

Now you can go ahead and edit the tests/users.js file to user these helper functions. You can check the file on github, but I’ll leave this as an exercise.

Now going back to the tests/entries/addEntry.js file, let’s add a bit to it. Before we add an entry, it should create and login in the user. Edit the file to add the following

const Users = require('../../db/models/Users');

const Entries = require('../../db/models/Entries');

const usershelper = require('../testHelpers/userHelper'); //Require the dev-dependencies

const chai = require('chai');

const chaiHttp = require('chai-http');

const server = require('../../bin/www');

const expect = chai.expect; chai.use(chaiHttp); describe('Entries', () => {

before(async () => {

// Empty out the entries

await Entries.query().delete();

// Empty out users

await Users.query().delete();

}); describe('Adding an entry', async () => {

it('should add an entry', async () => {

// First create a new user

let createReq = await usershelper

.addUser(chai.request(server));

expect(createReq.status).to.equal(201); // Now try login

let loginReq = await usershelper

.loginUser(

chai.request(server),

'pritojs@gmail.com',

'12345'

); // Make sure you were able to login

expect(loginReq.status).to.equal(200); }); });

});

Now we know that loginReq will contain the token. So we extract it and send a request to create and entry. This is where TDD comes into play. We have not created the API to add an entry. It doesn’t exist. However we can assume we will hit a url like POST /entries to add this entry which will have the params name , amount and type . It will also need to send the JWT in an Authorization header. So we pretend to test exactly that. Edit the file tests/entries/addEntry.js as follows

... // Make sure you were able to login

expect(loginReq.status).to.equal(200); // Extract token from the response

let { token } = loginReq.body;

// Send the add entry response

let addEntryReq = await chai

.request(server)

.post('/entries')

.set('Authorization', `Bearer ${token}`)

.send({

name: "Cat's food",

amount: 1000,

type: "EXPENSE"

}); // Make sure the request went well

expect(addEntryReq.status).to.equal(200); // Make sure the entry was added

let entry = await Entries.query().first(); expect(entry.name).to.equal("Cat's food");

expect(entry.amount).to.equal(1000);

expect(entry.type).to.equal("EXPENSE");

});

...

Now if you run the command mocha ./tests/entries/addEntry.js this test should fail. This is OKAY! this is what we wanted to write, a test which was failing.

Now before we proceed to actually writing the API, let’s create a new helper tests/testHelpers/entriesHelper.js so that we can create a methos to add entries. I leave this as an exercise. You can always check the file on github here.

You can find all the code till this point here.

The Auth

Now before we actually create the API, we are left with one piece of the puzzle. You can create the token, but how do you use it? Well, we will need the passport-http-bearer strategy for it. go ahead and run yarn add passport-http-bearer to install. Next edit the auth.js file as follows

const passport = require('passport-restify');

const LocalStrategy = require('passport-local').Strategy;

const BearerStrategy = require('passport-http-bearer').Strategy; const {

NotFoundError,

UnauthorizedError

} = require('restify-errors'); ... passport.use(new BearerStrategy(

async (token, done) => {

return done(null, token);

}

)); module.exports = passport;

Now we will be able to read the token, but we are not validating it. You can see that the token will be in the req.user property. Well so we need to read the token. To your helpers/crypto.js file, add the following function to help you decode the token.

const readWebToken = async (token) => {

return jwt.verify(token, appConfig.secretJWTKey);

}

Now edit the auth.js file to use this like so

... passport.use(new BearerStrategy(

async (token, done) => {

try {

// Here we decode the token

let tokenData = await cryptoHelper.readWebToken(token);

return done(null, tokenData);

}

catch (e) {

return done(new UnauthorizedError());

}



}

)); module.exports = passport;

Now we can use the token data, which will be the data stored.

The route

Now we’ll add the route for this. create a new file called routes/entries.js and to that file add the following

/**

* This file contains all the entry routes

*/ const Router = require('restify-router').Router;

const { InternalServerError, BadRequestError } = require('restify-errors');

const validate = require('restify-api-validation');

const Joi = require('joi');

const _ = require('lodash'); const routerInstance = new Router();

const entryHandler = require('../handlers/Entries');

const passport = require('../auth'); // This is a route to add new users

routerInstance.post(

'',

passport.authenticate('bearer', { session: false }),

async (req, res, next) => {

res.json(req.user);

}

); module.exports = routerInstance;

As you can see, we have used the same template as routes/user.js file in this folder (psst. I copied it ;) ). Now edit the routes/index.js file to add these routes as follows



* This function imports all the routes and adds them

* to the server provided.

*

*/ /*** This function imports all the routes and adds them* to the server provided. @param {Restify Server Instance} server*/ const userRoutes = require('./user');

const entryRoutes = require('./entries'); const addRoutes = (server) => {

// Add the user routes

userRoutes.applyRoutes(server, '/users');

// Add the entry routes

entryRoutes.applyRoutes(server, '/entries');

} module.exports = addRoutes;

If you now run the server and hit POST /entries you should be able to see the user data which was stored in the token. Well let’s move on and create the handler for this.

Create a file called handlers/Entries.js and add the following to it

const Entries = require('../db/models/Entries');

* Adds an entry to the entries table for the specified user

*

*

*

*

*

*/ /*** Adds an entry to the entries table for the specified user @param {integer} userId @param {string} name @param {number} amount @param {"INCOME" | "EXPENSE"} type*/ const addEntry = async (userId, name, amount, type) => {

return await Entries

.query()

.insert({

userId,

name,

amount,

type

})

.eager('user')

.omit(['password']);

}; module.exports = {

addEntry

};

This uses our model at db/models/Entry.js to add the entry. Note the user of eager and omit .

Please note, this has no validation. Validation needs to happen at the routing stage. So we edit the routes/entries.js file once again to add validation and call this function. Edit that file as follows

... // This is a route to add new users

routerInstance.post(

'',

validate(

{

body: {

name: Joi.string().required(),

amount: Joi.number().required(),

type: Joi.valid('INCOME','EXPENSE')

}

}

),

passport.authenticate('bearer', { session: false }),

async (req, res, next) => {

let {

name,

amount,

type

} = req.body; let userId = req.user.id;



let entry = await entryHandler.addEntry(

userId,

name,

amount,

type

); res.json(entry);

}

); module.exports = routerInstance;

Now if you try to hit the route, it should work. Also did you see what eager did?!

Finally it’s time to run our test. Run mocha ./tests/entries/addEntry.js . It should run as expected. So the second task in our user stories list is now over.

All code till this point is available here.

This brings us to the end of Part 2. I hope this has been helpful. Please CLAP if it was. If you liked it, you may also like part 3 which is coming soon-ly :)