From a5b26032cf016daf31cb229bb5f3509e95d4d0b9 Mon Sep 17 00:00:00 2001 From: Tomi Adebanjo Date: Wed, 5 Sep 2018 06:54:30 +0100 Subject: [PATCH] feat(comment): user can comment - add article routes - add seed to initialise a user, category and article - create controller to handle comment posts - create middleware validate user comments - update documentation to capture new route - write tests to test endpoint functionality - refactor usersValidations to confirm with ES6 syntax and not to send descriptive errors when logging in [Delivers ##159987407] feat(comment): user can comment - add article routes - add seed to initialise a user, category and article - create controller to handle comment posts - create middleware validate user comments - update documentation to capture new route - write tests to test endpoint functionality [Delivers ##159987407] feat(comment): user can comment - add article routes - add seed to initialise a user, category and article - create controller to handle comment posts - create middleware validate user comments - update documentation to capture new route - write tests to test endpoint functionality - refactor usersValidations to confirm with ES6 syntax and not to send descriptive errors when logging in [Delivers ##159987407] --- package.json | 2 +- server/app.js | 2 + server/controllers/commentsController.js | 31 ++++++ server/middleware/articlesValidations.js | 22 ++++ server/middleware/auth.js | 4 +- server/middleware/usersValidations.js | 55 ++++------ server/routes/articlesRoutes.js | 13 +++ server/seeders/20180905193504-demo-user.js | 17 +++ server/seeders/20180905193524-categories.js | 8 ++ server/seeders/20180905195555-demo-article.js | 13 +++ swagger.json | 40 ++++++- test/test.articles.js | 103 ++++++++++++++++++ test/test.spec.js | 53 +++++---- 13 files changed, 303 insertions(+), 60 deletions(-) create mode 100644 server/controllers/commentsController.js create mode 100644 server/middleware/articlesValidations.js create mode 100644 server/routes/articlesRoutes.js create mode 100644 server/seeders/20180905193504-demo-user.js create mode 100644 server/seeders/20180905193524-categories.js create mode 100644 server/seeders/20180905195555-demo-article.js create mode 100644 test/test.articles.js diff --git a/package.json b/package.json index 5cc069a..9db426f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A Social platform for the creative at heart", "main": "app.js", "scripts": { - "pretest": "NODE_ENV=test sequelize db:migrate", + "pretest": "NODE_ENV=test sequelize db:migrate && sequelize db:seed:all", "test": "NODE_ENV=test nyc mocha --exit --require babel-core/register", "posttest": "NODE_ENV=test sequelize db:migrate:undo:all", "start": "babel-node server/app.js", diff --git a/server/app.js b/server/app.js index b496432..e5549c5 100644 --- a/server/app.js +++ b/server/app.js @@ -5,6 +5,7 @@ import swaggerUi from 'swagger-ui-express'; import jsend from 'jsend'; import swaggerDocument from '../swagger.json'; import userRoutes from './routes/userRoutes'; +import articlesRoutes from './routes/articlesRoutes'; const app = express(); const urlencoded = bodyParser.urlencoded({ extended: false }); @@ -25,6 +26,7 @@ app.use(urlencoded); app.use(json); app.use('/api/v1/users', userRoutes); +app.use('/api/v1/articles', articlesRoutes); app.get('/', (req, res) => res.status(200).jsend.success({ message: 'Welcome to the sims program' diff --git a/server/controllers/commentsController.js b/server/controllers/commentsController.js new file mode 100644 index 0000000..f329ea3 --- /dev/null +++ b/server/controllers/commentsController.js @@ -0,0 +1,31 @@ +import validator from 'validator'; +import models from '../models/index'; + +const { Users, Comments } = models; + +const commentsController = { + create: (req, res) => { + const { articleId } = req.params; + const { content } = req.body; + const userId = req.currentUser.id; + + const userComment = { + content: validator.escape(content.trim()), + articleId: validator.escape(articleId), + userId + }; + + Comments + .create(userComment) + .then(comment => Users.findById(userId, { + attributes: ['id', 'username', 'firstname', 'lastname', 'createdAt', 'updatedAt'] + }) + .then(user => res.status(201).jsend.success({ user, comment }))) + .catch(error => res.status(500).jsend.error({ + message: 'There was a problem processing your request', + error + })); + } +}; + +export default commentsController; diff --git a/server/middleware/articlesValidations.js b/server/middleware/articlesValidations.js new file mode 100644 index 0000000..cdc988d --- /dev/null +++ b/server/middleware/articlesValidations.js @@ -0,0 +1,22 @@ +import helpers from '../helpers/helpers'; + +const articlesValidations = { + /** @description This method validates users' comments and replies + * @param {object} req The request object + * @param {object} res The response object + * @param {object} next the next middleware + * @returns {object} json response + */ + + validateComments: (req, res, next) => { + const { content } = req.body; + const { validString } = helpers; + + if (!validString(content)) { + return res.status(400).jsend.fail('Comment is empty'); + } + return next(); + } +}; + +export default articlesValidations; diff --git a/server/middleware/auth.js b/server/middleware/auth.js index c2536cf..116be43 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -34,8 +34,8 @@ const auth = (req, res, next) => { message: 'Failed to authenticate token! Valid token required', }); } - req.currentUser = decoded.id; - next(); + req.currentUser = decoded; + return next(); }); }; diff --git a/server/middleware/usersValidations.js b/server/middleware/usersValidations.js index b3210ce..b0c2d6a 100644 --- a/server/middleware/usersValidations.js +++ b/server/middleware/usersValidations.js @@ -1,4 +1,11 @@ -import helper from '../helpers/helpers'; +import helpers from '../helpers/helpers'; + +const { + checkProps, + validEmail, + validPassword, + validString +} = helpers; const usersValidations = { /** @description This method helps validate a user signups @@ -12,8 +19,9 @@ const usersValidations = { let messages = []; // Check the passed body for required properties - const { valid, invalidMessages } = helper - .checkProps(req.body, 'username', 'email', 'password'); + const { + valid, invalidMessages + } = checkProps(req.body, 'username', 'email', 'password'); if (!valid) { return res.status(400) @@ -23,21 +31,20 @@ const usersValidations = { } // Validate the email address provided - if (!helper.validEmail(req.body.email)) { + if (!validEmail(req.body.email)) { status = 'fail'; messages.push('Invalid email provided'); } - // Validate the password provided - if (!helper.validPassword(req.body.password).valid) { + if (!validPassword(req.body.password).valid) { status = 'fail'; messages = messages - .concat(helper.validPassword(req.body.password).invalidMessages); + .concat(validPassword(req.body.password).invalidMessages); } // validate the firstName; - if (!helper.validString(req.body.username)) { + if (!validString(req.body.username)) { status = 'fail'; messages.push('username cannot be an empty string'); } @@ -59,38 +66,14 @@ const usersValidations = { * @returns {object} undefined */ validateLogin: (req, res, next) => { - let status = 'success'; - let messages = []; - // Check the passed body for required properties - const { valid, invalidMessages } = helper + const { valid, invalidMessages } = helpers .checkProps(req.body, 'email', 'password'); if (!valid) { - return res.status(400) - .jsend.fail({ - messages: invalidMessages - }); - } - - // Validate the email address provided - if (!helper.validEmail(req.body.email)) { - status = 'fail'; - messages.push('Invalid email provided'); - } - - - // Validate the password provided - if (!helper.validPassword(req.body.password).valid) { - status = 'fail'; - messages = messages.concat(helper.validPassword(req.body.password).invalidMessages); - } - - if (status === 'fail') { - return res.status(400) - .jsend.fail({ - messages - }); + return res.status(400).jsend.fail({ + messages: invalidMessages + }); } return next(); } diff --git a/server/routes/articlesRoutes.js b/server/routes/articlesRoutes.js new file mode 100644 index 0000000..926f1a2 --- /dev/null +++ b/server/routes/articlesRoutes.js @@ -0,0 +1,13 @@ +import express from 'express'; +import commentsController from '../controllers/commentsController'; +import articlesValidations from '../middleware/articlesValidations'; +import auth from '../middleware/auth'; + +const articlesRoutes = express.Router(); + +const { create } = commentsController; +const { validateComments } = articlesValidations; + +articlesRoutes.post('/:articleId', auth, validateComments, create); + +export default articlesRoutes; diff --git a/server/seeders/20180905193504-demo-user.js b/server/seeders/20180905193504-demo-user.js new file mode 100644 index 0000000..9dd48bd --- /dev/null +++ b/server/seeders/20180905193504-demo-user.js @@ -0,0 +1,17 @@ +module.exports = { + up: queryInterface => queryInterface.bulkInsert('Users', [{ + username: 'johndoe', + firstname: 'John', + lastname: 'Doe', + email: 'johndoe@gmail.com', + password: 'johndoe', + bio: 'I like to eat', + image: 'someimgurl', + premium: true, + isVerified: true, + interests: ['Entertainment', 'Science'], + createdAt: '2018-09-05', + updatedAt: '2018-09-05' + }]), + down: queryInterface => queryInterface.bulkDelete('Users') +}; diff --git a/server/seeders/20180905193524-categories.js b/server/seeders/20180905193524-categories.js new file mode 100644 index 0000000..f3ea181 --- /dev/null +++ b/server/seeders/20180905193524-categories.js @@ -0,0 +1,8 @@ +module.exports = { + up: queryInterface => queryInterface.bulkInsert('Categories', [{ + name: 'Science', + createdAt: '2018-09-05', + updatedAt: '2018-09-05' + }]), + down: queryInterface => queryInterface.bulkDelete('Categories') +}; diff --git a/server/seeders/20180905195555-demo-article.js b/server/seeders/20180905195555-demo-article.js new file mode 100644 index 0000000..182df2f --- /dev/null +++ b/server/seeders/20180905195555-demo-article.js @@ -0,0 +1,13 @@ +module.exports = { + up: queryInterface => queryInterface.bulkInsert('Articles', [{ + slug: 'slug-here', + title: 'My Life', + description: 'About my life', + body: 'This is the content of the story of my life', + userId: 1, + categoryId: 1, + createdAt: '2018-09-05', + updatedAt: '2018-09-05' + }]), + down: queryInterface => queryInterface.bulkDelete('Articles') +}; diff --git a/swagger.json b/swagger.json index 52935aa..eff76b7 100644 --- a/swagger.json +++ b/swagger.json @@ -93,7 +93,33 @@ } } } - } + }, + "/api/v1/articles/:articleId": { + "post": { + "tags": [ "Articles" ], + "summary": "Route for authenticated users to post comments on an article", + "description": "Returns the user and the user's comment", + "parameters": [ + { + "in": "body", + "name": "body", + "description": "comment object to be returned when a user makes a post request", + "required": true, + "schema": { + "$ref": "#/definitions/Comment" + } + } + ], + "responses": { + "201": { + "description": "Comment posted successfully" + }, + "400": { + "description": "Comment is empty" + } + } + } + } }, "definitions": { "Signup": { @@ -131,6 +157,18 @@ "json": { "name": "User" } + }, + "Comment": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "Great post!" + } + }, + "json": { + "name": "Comment" + } } } } diff --git a/test/test.articles.js b/test/test.articles.js new file mode 100644 index 0000000..314fe7d --- /dev/null +++ b/test/test.articles.js @@ -0,0 +1,103 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import app from '../server/app'; + +chai.use(chaiHttp); +const { should } = chai; +should(); + +let token; + +describe('Tests for Articles', () => { + describe('Create comment tests', () => { + before((done) => { + chai + .request(app) + .post('/api/v1/users/auth/signup') + .send({ + username: 'enjames', + email: 'enjames@james.com', + password: 'pasS1234', + }) + .end((err, res) => { + token = res.body.data.token; + res.body.should.have.property('data'); + done(); + }); + }); + + it('User should not be able to comment if not authenticated', (done) => { + chai + .request(app) + .post('/api/v1/articles/1') + .send({ + content: 'Your post was not inspiring.' + }) + .end((err, res) => { + res.body.status.should.equal('error'); + res.body.message.should.equal('No token provided'); + done(); + }); + }); + + it('User should receive an error if authentication fails', (done) => { + chai + .request(app) + .post('/api/v1/articles/1') + .set('Authorization', '8beccb8ef75986c7096888907ddf4165889255315b67be782a3333eeeeee') + .send({ + content: 'Your post was not inspiring.' + }) + .end((err, res) => { + res.body.status.should.equal('error'); + res.body.message.should.equal('Failed to authenticate token! Valid token required'); + done(); + }); + }); + + it('It should return an object containing a user and a comment object', (done) => { + chai + .request(app) + .post('/api/v1/articles/1') + .set('Authorization', token) + .send({ + content: '' + }) + .end((err, res) => { + res.body.status.should.equal('fail'); + res.body.data.should.equal('Comment is empty'); + done(); + }); + }); + + it('It should return an object containing a user and a comment object', (done) => { + chai + .request(app) + .post('/api/v1/articles/1') + .set('Authorization', token) + .send({ + content: 'Your post was not inspiring.' + }) + .end((err, res) => { + res.body.data.should.have.property('user'); + res.body.data.comment.content.should.equal('Your post was not inspiring.'); + done(); + }); + }); + + it('It should return an object containing a user and a comment object', (done) => { + chai + .request(app) + .post('/api/v1/articles/13431') + .set('Authorization', token) + .send({ + content: 'Your post was not inspiring.' + }) + .end((err, res) => { + res.body.status.should.equal('error'); + res.body.message.should.equal('There was a problem processing your request'); + done(); + }); + }); + }); +}); diff --git a/test/test.spec.js b/test/test.spec.js index 44e87cb..c06cb93 100644 --- a/test/test.spec.js +++ b/test/test.spec.js @@ -40,8 +40,7 @@ describe('TEST ALL ENDPOINT', () => { chai .request(app) .post('/api/v1/users/auth/login') - .send({ - }) + .send({}) .end((err, res) => { res.body.should.be.an('object'); res.body.should.have.property('status'); @@ -96,8 +95,8 @@ describe('TEST ALL ENDPOINT', () => { .end((err, res) => { res.body.should.be.an('object'); res.body.should.have.property('status'); - res.body.data.should.have.property('messages'); - res.body.data.messages.should.eql(['Invalid email provided']); + res.body.should.have.property('message'); + res.body.message.should.eql('Invalid credentials supplied'); done(); }); }); @@ -118,22 +117,6 @@ describe('TEST ALL ENDPOINT', () => { done(); }); }); - it('login password error', (done) => { - chai - .request(app) - .post('/api/v1/users/auth/login') - .send({ - email: 'test.tester@email.com', - password: 'asswordlksndv' - }) - .end((err, res) => { - res.body.should.be.an('object'); - res.body.should.have.property('status'); - res.body.data.should.have.property('messages'); - res.body.data.messages.should.eql(['Password must include at least one uppercase and lowercase character']); - done(); - }); - }); it('signup password error', (done) => { chai .request(app) @@ -237,6 +220,36 @@ describe('USER SIGN IN TEST', () => { }); }); + it('should return an error when password does not meet set rules', (done) => { + chai + .request(app) + .post('/api/v1/users/auth/login') + .send({ + email: '', + password: 'passwwor' + }) + .end((err, res) => { + expect(res.body.status).to.equal('fail'); + expect(res.body.data.messages[0]).to.equal('Please provide email'); + done(); + }); + }); + + it('should return an error when email is invalid', (done) => { + chai + .request(app) + .post('/api/v1/users/auth/login') + .send({ + email: 'someemail.co', + password: 'passwwor' + }) + .end((err, res) => { + expect(res.body.status).to.equal('error'); + expect(res.body.message).to.equal('Invalid credentials supplied'); + done(); + }); + }); + it('should return sign in successful and return token', (done) => { chai .request(app)