From 53dc8fb67e2cdd130ba151ff51f1a7949d80b359 Mon Sep 17 00:00:00 2001 From: Mcdavid Emereuwa Date: Wed, 10 Jul 2019 18:38:03 +0100 Subject: [PATCH] feature(bus): add create bus feature - add endpoint to create bus - write test to check for edge cases [Finishes #167195764] --- src/controllers/Auth.js | 4 +-- src/controllers/Bus.js | 40 +++++++++++++++++++++ src/helpers/utils.js | 25 ++++++++++++-- src/helpers/validateInput.js | 32 ++++++++++++++++- src/index.js | 2 ++ src/routes/bus.js | 13 +++++++ tests/__mocks__/bus.mocks.js | 14 ++++++++ tests/controllers/bus.spec.js | 65 +++++++++++++++++++++++++++++++++++ 8 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 src/controllers/Bus.js create mode 100644 src/routes/bus.js create mode 100644 tests/__mocks__/bus.mocks.js create mode 100644 tests/controllers/bus.spec.js diff --git a/src/controllers/Auth.js b/src/controllers/Auth.js index c8b7c04..f8cb99d 100644 --- a/src/controllers/Auth.js +++ b/src/controllers/Auth.js @@ -31,7 +31,7 @@ const Auth = { moment(new Date()) ]; const { rows } = await db.query(createQuery, values); - const token = createToken(rows[0].id); + const token = createToken(rows[0].id, rows[0].is_admin); return handleServerResponse(res, 201, { user_id: rows[0].id, is_admin: rows[0].is_admin, @@ -62,7 +62,7 @@ const Auth = { if (!isPassword(password, rows[0].password)) { return handleServerResponseError(res, 403, 'Password incorrect'); } - const token = createToken(rows[0].id); + const token = createToken(rows[0].id, rows[0].is_admin); return handleServerResponse(res, 200, { user_id: rows[0].id, token }); } catch (error) { return handleServerError(res, error); diff --git a/src/controllers/Bus.js b/src/controllers/Bus.js new file mode 100644 index 0000000..31259c0 --- /dev/null +++ b/src/controllers/Bus.js @@ -0,0 +1,40 @@ +import moment from 'moment'; +import db from './db'; +import { + handleServerError, + handleServerResponse, + handleServerResponseError, +} from '../helpers/utils'; + +export default { + async create(req, res) { + const { + // eslint-disable-next-line camelcase + model, numberPlate, manufacturer, year, capacity + } = req.body; + try { + const createQuery = `INSERT INTO + Buses(model, number_plate, manufacturer, year, capacity, created_date, modified_date) + VALUES($1, $2, $3, $4, $5, $6, $7) + returning *`; + const values = [ + model.trim().toLowerCase(), + numberPlate.trim().toLowerCase(), + manufacturer.trim().toLowerCase(), + year, + capacity, + moment(new Date()), + moment(new Date()) + ]; + const { rows } = await db.query(createQuery, values); + return handleServerResponse(res, 201, { + bus: rows[0] + }); + } catch (error) { + if (error.routine === '_bt_check_unique') { + return handleServerResponseError(res, 409, `Bus with number plate:- ${numberPlate.trim().toLowerCase()} already exists`); + } + handleServerError(res, error); + } + }, +}; diff --git a/src/helpers/utils.js b/src/helpers/utils.js index fd501b5..36e86bf 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -69,13 +69,14 @@ export const isPassword = (password, hash) => bcrypt.compareSync(password, hash) /** * createToken * @param {Number} id user id gotten from DATABASE_URL + * @param {Number} isAdmin value of if user is an admin * @description creates new jwt token for authentication * @returns {String} newly created jwt */ -export const createToken = (id) => { +export const createToken = (id, isAdmin) => { const token = jwt.sign( { - id + id, isAdmin }, process.env.SECRET, { expiresIn: '7d' } ); @@ -109,3 +110,23 @@ export const hasToken = async (req, res, next) => { return handleServerResponseError(res, 403, error); } }; + +/** + * @method hasToken + * @param {*} req + * @param {*} res + * @param {*} next + * @returns {Object} response object + */ +export const isAdmin = async (req, res, next) => { + const token = req.body.token || req.headers['x-access-token']; + try { + const decoded = await jwt.verify(token, process.env.SECRET); + if (!decoded.isAdmin) { + return handleServerResponseError(res, 403, 'You are not authorized to access this endpoint'); + } + return next(); + } catch (error) { + return handleServerResponseError(res, 403, error); + } +}; diff --git a/src/helpers/validateInput.js b/src/helpers/validateInput.js index 6afc589..9e5d292 100644 --- a/src/helpers/validateInput.js +++ b/src/helpers/validateInput.js @@ -54,8 +54,38 @@ const signinInput = (req, res, next) => { return next(); }; +/** + * @function + * @param {*} req + * @param {*} res + * @param {*} next + * @description validates create bus input + * @returns {Response | RequestHandler} error or request handler + */ +const createBusInput = (req, res, next) => { + const { + // eslint-disable-next-line camelcase + model, manufacturer, year, numberPlate, capacity + } = req.body; + const schema = Joi.object().keys({ + model: Joi.string().required(), + manufacturer: Joi.string().required(), + year: Joi.string().trim().required(), + numberPlate: Joi.string().required(), + capacity: Joi.number().required() + }); + const result = Joi.validate({ + model, manufacturer, year, numberPlate, capacity + }, schema); + if (result.error) { + return handleServerResponseError(res, 401, result.error.details[0].message); + } + return next(); +}; + export default { validateSignup: signupInput, - validateSignin: signinInput + validateSignin: signinInput, + validateCreateBus: createBusInput }; diff --git a/src/index.js b/src/index.js index 921e318..bb9760d 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ import cors from 'cors'; import morgan from 'morgan'; import { logger } from './helpers/utils'; import auth from './routes/auth'; +import bus from './routes/bus'; dotenv.config(); @@ -27,6 +28,7 @@ app.get('/api/v1', (req, res) => res.status(200).send({ })); app.use('/api/v1/auth', auth); +app.use('/api/v1/bus', bus); app.listen(port); logger().info(`app running on port ${port}`); diff --git a/src/routes/bus.js b/src/routes/bus.js new file mode 100644 index 0000000..95cd69d --- /dev/null +++ b/src/routes/bus.js @@ -0,0 +1,13 @@ +import express from 'express'; +import Bus from '../controllers/Bus'; +import ValidateInput from '../helpers/validateInput'; +import { isAdmin, hasToken } from '../helpers/utils'; + +const { create } = Bus; +const { validateCreateBus } = ValidateInput; + +const router = express.Router(); + +router.post('/', hasToken, isAdmin, validateCreateBus, create); + +export default router; diff --git a/tests/__mocks__/bus.mocks.js b/tests/__mocks__/bus.mocks.js new file mode 100644 index 0000000..b58c988 --- /dev/null +++ b/tests/__mocks__/bus.mocks.js @@ -0,0 +1,14 @@ +export const bus = { + model: 'Hiace', + manufacturer: 'Toyota', + year: '2012', + numberPlate: 'fh3du5', + capacity: 16 +}; + +export const incompleteBus = { + model: 'Hiace', + manufacturer: 'Toyota', + year: '2012', + capacity: 16 +}; diff --git a/tests/controllers/bus.spec.js b/tests/controllers/bus.spec.js new file mode 100644 index 0000000..22d264d --- /dev/null +++ b/tests/controllers/bus.spec.js @@ -0,0 +1,65 @@ +import chai from 'chai'; +import api from '../test.config'; +import { + normalUser, adminUser +} from '../__mocks__/auth.mocks'; +import { bus } from '../__mocks__/bus.mocks'; + +const { expect } = chai; +let adminToken, + userToken; + +describe('Bus controller', () => { + it('should login an admin', async () => { + const server = await api.post('/api/v1/auth/signin') + .type('form') + .set('Content-Type', 'application/json') + .send(adminUser); + adminToken = server.body.data.token; + expect(server.statusCode).to.equal(200); + }); + + it('should create a new bus', async () => { + const server = await api.post('/api/v1/bus') + .type('form') + .set('Content-Type', 'application/json') + .set('x-access-token', adminToken) + .send(bus); + expect(server.statusCode).to.equal(201); + }); + + it('should not create a new bus if token is not present', async () => { + const server = await api.post('/api/v1/bus') + .type('form') + .set('Content-Type', 'application/json') + .send(bus); + expect(server.statusCode).to.equal(403); + }); + + it('should signin a user', async () => { + const server = await api.post('/api/v1/auth/signin') + .type('form') + .set('Content-Type', 'application/json') + .send(normalUser); + userToken = server.body.data.token; + expect(server.statusCode).to.equal(200); + }); + + it('should not create a new bus if user is not an admin', async () => { + const server = await api.post('/api/v1/bus') + .type('form') + .set('Content-Type', 'application/json') + .set('x-access-token', userToken) + .send(bus); + expect(server.statusCode).to.equal(403); + }); + + it('should not create a new bus if plate number exists', async () => { + const server = await api.post('/api/v1/bus') + .type('form') + .set('Content-Type', 'application/json') + .set('x-access-token', adminToken) + .send(bus); + expect(server.statusCode).to.equal(409); + }); +});