From a5f86fe6621e493d56f4590dd6a8f4cdc08488d3 Mon Sep 17 00:00:00 2001 From: Ayooluwa Oyewo Date: Wed, 11 Sep 2019 19:15:27 +0100 Subject: [PATCH] feat(request comments): request comments - setup controller for user to comment on requests - setup route for user to comment on requests - setup unit test for user to comment on requests route [Delivers #167750066] --- src/controllers/commentsController.js | 124 +++++++ src/controllers/index.js | 3 +- src/database/seeders/create-6-request.js | 29 ++ src/database/seeders/create-7-comments.js | 29 ++ src/models/Comment.js | 1 + src/models/Request.js | 2 +- src/routes/api/comments.js | 21 ++ src/routes/index.js | 4 + src/test/controllers/comments.test.js | 386 ++++++++++++++++++++++ src/test/index.test.js | 39 +-- src/validation/index.js | 3 +- src/validation/validators/rules.js | 4 + 12 files changed, 623 insertions(+), 22 deletions(-) create mode 100644 src/controllers/commentsController.js create mode 100644 src/database/seeders/create-6-request.js create mode 100644 src/database/seeders/create-7-comments.js create mode 100644 src/routes/api/comments.js create mode 100644 src/test/controllers/comments.test.js diff --git a/src/controllers/commentsController.js b/src/controllers/commentsController.js new file mode 100644 index 0000000..8e96d9e --- /dev/null +++ b/src/controllers/commentsController.js @@ -0,0 +1,124 @@ +import models from '../models'; +import { successResponse, errorResponse, status } from '../utils'; + +const { Comments, Requests } = models; + +/** + * @class CommentsController + * @description Controllers for handling travel requests comments + * @exports CommentsController + */ +class CommentsController { + /** + * @method addComment + * @description Method to add comment to requests + * @param {object} req - The Request Object + * @param {object} res - The Response Object + * @returns {object} Newly added request comment + */ + static async addComment(req, res) { + const { userId } = req.user; + const { requestId } = req.params; + const { comment } = req.body; + try { + const existingRequest = await Requests.findOne({ + where: { id: requestId } + }); + if (!existingRequest) { + return errorResponse(res, status.notfound, 'Request does not exist'); + } + const commentAdded = await Comments.create({ comment, userId, requestId }); + const response = commentAdded.toJSON(); + return successResponse(res, status.created, 'Comment added successfully', response); + } catch (error) { + return errorResponse(res, status.error, 'Error adding comment'); + } + } + + /** + * @method getSingleComment + * @description Method to get a comment + * @param {object} req - The Request Object + * @param {object} res - The Response Object + * @returns {object} retrieved comment details + */ + static async getCommentById(req, res) { + const { commentId } = req.params; + try { + const getComment = await Comments.findByPk(commentId); + if (!getComment) { + return errorResponse(res, status.notfound, 'Comment not found'); + } + const response = getComment.toJSON(); + return successResponse(res, status.success, 'Comment retrieved successfully', response); + } catch (error) { + return errorResponse(res, status.error, 'Error retrieving comment'); + } + } + + /** + * @method updateComment + * @description Method to update comment + * @param {object} req - The Request Object + * @param {object} res - The Response Object + * @returns {object} updated comment details + */ + static async updateCommentById(req, res) { + const { commentId } = req.params; + const { comment } = req.body; + try { + const getComment = await Comments.findOne({ where: { id: commentId } }); + if (!getComment) { + return errorResponse(res, status.notfound, 'Comment not found'); + } + await Comments.update({ comment }, { where: { id: commentId } }); + return successResponse(res, status.success, 'Comment updated successfully'); + } catch (error) { + return errorResponse(res, status.error, 'Error updating comment'); + } + } + + /** + * @method deleteComment + * @description Method to delete comment + * @param {object} req - The Request Object + * @param {object} res - The Response Object + * @returns {object} deleted comment details + */ + static async deleteCommentById(req, res) { + const { commentId } = req.params; + try { + const getComment = await Comments.findByPk(commentId); + if (!getComment) { + return errorResponse(res, status.notfound, 'Comment not found'); + } + await Comments.destroy({ where: { id: commentId } }); + return successResponse(res, status.success, 'Comment deleted successfully'); + } catch (error) { + return errorResponse(res, status.error, 'Error deleting comment'); + } + } + + /** + * @method getAllCommentsOnRequest + * @description Method to get all comments on requests + * @param {object} req - The Request Object + * @param {object} res - The Response Object + * @returns {object} retrieved comments details + */ + static async getAllCommentsOnRequest(req, res) { + const { requestId } = req.params; + try { + const existingRequest = await Requests.findOne({ where: { id: requestId } }); + if (!existingRequest) { + return errorResponse(res, status.notfound, 'Request not found'); + } + const response = await Comments.findAll({ where: { requestId } }); + return successResponse(res, status.success, 'Comments retrieved successfully', response); + } catch (error) { + return errorResponse(res, status.error, 'Error retrieving comments'); + } + } +} + +export default CommentsController; diff --git a/src/controllers/index.js b/src/controllers/index.js index 6523e48..deddf99 100755 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -2,8 +2,9 @@ import UsersController from './users'; import ResetPasswordController from './resetPassword'; import AccommodationController from './accommodationController'; import RoomController from './roomController'; +import CommentsController from './commentsController'; export { UsersController, ResetPasswordController, - AccommodationController, RoomController + AccommodationController, RoomController, CommentsController }; diff --git a/src/database/seeders/create-6-request.js b/src/database/seeders/create-6-request.js new file mode 100644 index 0000000..1b21489 --- /dev/null +++ b/src/database/seeders/create-6-request.js @@ -0,0 +1,29 @@ +export default { + up: queryInterface => queryInterface.bulkInsert( + 'Requests', + [ + { + id: '2b770fbc-76e6-4b5a-afab-882759fd1f06', + status: 'pending', + accommodationId: '2b770fbc-76e6-4b5a-afab-882759fd1f06', + userId: 'e71c28fd-73d8-4d92-9125-ab3d022093b9' + }, + { + id: 'b356097c-c6d0-4a3d-85f6-33bc2595c974', + status: 'rejected', + accommodationId: '2b770fbc-76e6-4b5a-afab-882759fd1f06', + userId: 'e71c28fd-73d8-4d92-9125-ab3d022093b0' + }, + { + id: '777f640e-a2ff-45ee-9ce1-bf37645c42d6', + status: 'approved', + accommodationId: '2b770fbc-76e6-4b5a-afab-882759fd1f06', + userId: '7aa38d4e-7fbf-4067-8821-9c27d2fb6e3a' + }, + ], + {} + ), + + down: queryInterface => queryInterface.bulkDelete('Requests', null, {}) + }; + \ No newline at end of file diff --git a/src/database/seeders/create-7-comments.js b/src/database/seeders/create-7-comments.js new file mode 100644 index 0000000..046fb7b --- /dev/null +++ b/src/database/seeders/create-7-comments.js @@ -0,0 +1,29 @@ +export default { + up: queryInterface => queryInterface.bulkInsert( + 'Comments', + [ + { + id: '2b770fbc-76e6-4b5a-afab-882759fd1f06', + comment: 'my added comment', + userId: 'e71c28fd-73d8-4d92-9125-ab3d022093b9', + requestId: '2b770fbc-76e6-4b5a-afab-882759fd1f06' + }, + { + id: 'b356097c-c6d0-4a3d-85f6-33bc2595c974', + comment: 'my added comment', + userId: 'e71c28fd-73d8-4d92-9125-ab3d022093b0', + requestId: '2b770fbc-76e6-4b5a-afab-882759fd1f06' + }, + { + id: '777f640e-a2ff-45ee-9ce1-bf37645c42d6', + comment: 'mu added comment', + userId: '7aa38d4e-7fbf-4067-8821-9c27d2fb6e3a', + requestId: '2b770fbc-76e6-4b5a-afab-882759fd1f06', + }, + ], + {} + ), + + down: queryInterface => queryInterface.bulkDelete('Comments', null, {}) + }; + \ No newline at end of file diff --git a/src/models/Comment.js b/src/models/Comment.js index fb0e239..93218c2 100755 --- a/src/models/Comment.js +++ b/src/models/Comment.js @@ -7,6 +7,7 @@ export default (sequelize, DataTypes) => { }, {}); Comment.associate = (models) => { Comment.belongsTo(models.Users, { as: 'theComment', foreignKey: 'userId' }); + Comment.belongsTo(models.Requests, { as: 'theRequest', foreignKey: 'requestId' }); }; return Comment; }; diff --git a/src/models/Request.js b/src/models/Request.js index 7932400..a291828 100755 --- a/src/models/Request.js +++ b/src/models/Request.js @@ -7,7 +7,7 @@ export default (sequelize, DataTypes) => { }, }, {}); Request.associate = (models) => { - Request.hasMany(models.Comments, { as: 'requestComments', foreignKey: 'reqId' }); + Request.hasMany(models.Comments, { as: 'requestComments', foreignKey: 'requestId' }); Request.hasMany(models.Trips, { as: 'requestTrips', foreignKey: 'reqId' }); }; return Request; diff --git a/src/routes/api/comments.js b/src/routes/api/comments.js new file mode 100644 index 0000000..f7e6629 --- /dev/null +++ b/src/routes/api/comments.js @@ -0,0 +1,21 @@ +import { Router } from 'express'; +import { CommentsController } from '../../controllers'; +import middlewares from '../../middlewares'; + +const router = new Router(); + +const { validate, Authenticate } = middlewares; +const { verifyToken } = Authenticate; + +const { + addComment, getCommentById, deleteCommentById, updateCommentById, getAllCommentsOnRequest +} = CommentsController; + +router.post('/requests/:requestId/comments', verifyToken, validate('addComment'), addComment); +router.get('/comments/:commentId', verifyToken, getCommentById); +router.patch('/comments/:commentId', verifyToken, validate('addComment'), updateCommentById); +router.delete('/comments/:commentId', verifyToken, deleteCommentById); +router.get('/requests/:requestId/comments', verifyToken, getAllCommentsOnRequest); + + +export default router; diff --git a/src/routes/index.js b/src/routes/index.js index f9c5665..d308445 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,6 +4,8 @@ import authRoutes from './api/auth'; import resetPasswordRoute from './api/resetPassword'; import accommodationRoute from './api/accommodation'; import roomRoute from './api/room'; +import commentsRoute from './api/comments'; + const router = express.Router(); @@ -12,5 +14,7 @@ router.use('/users', userRoute); router.use('/', resetPasswordRoute); router.use('/', accommodationRoute); router.use('/', roomRoute); +router.use('/', commentsRoute); + export default router; diff --git a/src/test/controllers/comments.test.js b/src/test/controllers/comments.test.js new file mode 100644 index 0000000..c53dcf5 --- /dev/null +++ b/src/test/controllers/comments.test.js @@ -0,0 +1,386 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { status } from '../../utils'; +import server from '../../index'; +import models from '../../models'; +import { CommentsController } from '../../controllers'; + +const { expect } = chai; +chai.use(chaiHttp); +chai.use(sinonChai); +chai.should(); +const user = { + email: 'funmi1@gmail.com', + password: 'funmi1234', +}; + +const signinRoute = '/api/v1/users/signin'; + +const comment = { + comment: 'my added comment' +}; + +const nocomment = { + comment: '' +}; + +// ADD comment Testing +describe('/POST add new comment', () => { + it('it should not ADD a comment if auth token is not provided', (done) => { + chai.request(server) + .post('/api/v1/requests/2b770fbc-76e6-4b5a-afab-882759fd1f06/comments') + .send(comment) + .end((err, res) => { + res.should.have.status(status.error); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Server error'); + done(err); + }); + }); + + it('it should return a response requestId is not valid', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + + chai.request(server) + .post('/api/v1/requests/2b770fbc-76e6-4b5a-afab-882759fd1f07/comments') + .set('Authorization', token) + .send(comment) + .end((err, res) => { + res.should.have.status(status.notfound); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Request does not exist'); + done(err); + }); + }); + }); + + it('Should return error for invalid comment data', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .post('/api/v1/requests/2b770fbc-76e6-4b5a-afab-882759fd1f07/comments') + .set('Authorization', token) + .send(nocomment) + .end((err, res) => { + res.should.have.status(status.unprocessable); + res.body.should.be.a('object'); + res.body.should.have.property('errors'); + done(err); + }); + }); + }); + + it('it should post comment', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + + chai.request(server) + .post('/api/v1/requests/2b770fbc-76e6-4b5a-afab-882759fd1f06/comments') + .set('Authorization', token) + .send(comment) + .end((err, res) => { + res.should.have.status(status.created); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comment added successfully'); + done(err); + }); + }); + }); + + it('fakes server when adding an comment', async () => { + const req = { + body: { + comment: 'my added comment' + }, + user: { + userId: 'e71c28fd-73d8-4d92-9125-ab3d022093b9', + + }, + params: { + requestId: '2b770fbc-76e6-4b5a-afab-882759fd1f06' + } + }; + const res = { + status: () => { }, + json: () => { }, + }; + sinon.stub(res, 'status').returnsThis(); + sinon.stub(models.Comments, 'create').throws(); + + await CommentsController.addComment(req, res); + expect(res.status).to.have.been.calledWith(status.error); + }); +}); + + +// GET Single comment Testing +describe('/GET Single comment', () => { + it('it should return a response commentId is not valid', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .get('/api/v1/comments/2b770fbc-76e6-4b5a-afab-882759fd1f04') + .set('Authorization', token) + .end((err, res) => { + res.should.have.status(status.notfound); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comment not found'); + done(err); + }); + }); + }); + + it('Should return comment', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .get('/api/v1/comments/2b770fbc-76e6-4b5a-afab-882759fd1f06') + .set('Authorization', token) + .end((err, res) => { + res.should.have.status(status.success); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comment retrieved successfully'); + res.body.should.have.property('data'); + done(err); + }); + }); + }); + + it('fakes server when retreiving a comment', async () => { + const req = { + params: { + commentId: '2b770fbc-76e6-4b5a-afab-882759fd1f06' + } + }; + const res = { + status: () => { }, + json: () => { }, + }; + sinon.stub(res, 'status').returnsThis(); + sinon.stub(models.Comments, 'findByPk').throws(); + + await CommentsController.getCommentById(req, res); + expect(res.status).to.have.been.calledWith(status.error); + }); +}); + +// UPDATE Single comment Testing +describe('/PATCH Single comment', () => { + it('it should return a response commentId is not valid', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .patch('/api/v1/comments/2b770fbc-76e6-4b5a-afab-882759fd1f04') + .set('Authorization', token) + .send(comment) + .end((err, res) => { + res.should.have.status(status.notfound); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comment not found'); + done(err); + }); + }); + }); + it('Should return error for invalid comment data', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .patch('/api/v1/comments/2b770fbc-76e6-4b5a-afab-882759fd1f06') + .set('Authorization', token) + .send(nocomment) + .end((err, res) => { + res.should.have.status(status.unprocessable); + res.body.should.be.a('object'); + res.body.should.have.property('errors'); + done(err); + }); + }); + }); + it('Should update a comment', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .patch('/api/v1/comments/2b770fbc-76e6-4b5a-afab-882759fd1f06') + .set('Authorization', token) + .send(comment) + .end((err, res) => { + res.should.have.status(status.success); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comment updated successfully'); + done(err); + }); + }); + }); + + it('fakes server when updating a comment', async () => { + const req = { + body: { + comment: 'updated comment' + }, + params: { + commentId: '2b770fbc-76e6-4b5a-afab-882759fd1f06' + } + }; + const res = { + status: () => { }, + json: () => { }, + }; + sinon.stub(res, 'status').returnsThis(); + sinon.stub(models.Comments, 'update').throws(); + + await CommentsController.updateCommentById(req, res); + expect(res.status).to.have.been.calledWith(status.error); + }); +}); + +// DELETE Single comment Testing +describe('/DELETE Single comment', () => { + it('it should return a response when comment is deleted', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .delete('/api/v1/comments/2b770fbc-76e6-4b5a-afab-882759fd1f06') + .set('Authorization', token) + .end((err, res) => { + res.should.have.status(status.success); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comment deleted successfully'); + done(err); + }); + }); + }); + + it('it should return a response when commentId is not valid', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .delete('/api/v1/comments/777f640e-w4gg-45ee-8yuj-bf37645c42d6') + .set('Authorization', token) + .end((err, res) => { + res.should.have.status(status.notfound); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comment not found'); + done(err); + }); + }); + }); + + it('fakes server error when deleting an comment', async () => { + const req = { + params: { + commentId: 'b356097c-c6d0-4a3d-85f6-33bc2595c974' + } + }; + const res = { + status: () => { }, + json: () => { }, + }; + sinon.stub(res, 'status').returnsThis(); + sinon.stub(models.Comments, 'destroy').throws(); + + await CommentsController.deleteCommentById(req, res); + expect(res.status).to.have.been.calledWith(status.error); + }); +}); + +// GET all comment Testing +describe('/GET All request comment', () => { + it('it should return a response commentId is not valid', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .get('/api/v1/requests/2b770fbc-76e6-4b5a-afab-882759fd1f07/comments') + .set('Authorization', token) + .end((err, res) => { + res.should.have.status(status.notfound); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Request not found'); + done(err); + }); + }); + }); + + it('Should return comments', (done) => { + chai + .request(server) + .post(signinRoute) + .send(user) + .end((error, response) => { + const token = `Bearer ${response.body.data.token}`; + chai.request(server) + .get('/api/v1/requests/2b770fbc-76e6-4b5a-afab-882759fd1f06/comments') + .set('Authorization', token) + .end((err, res) => { + res.should.have.status(status.success); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('Comments retrieved successfully'); + res.body.should.have.property('data'); + done(err); + }); + }); + }); + + it('fakes server when retreiving all comments', async () => { + const req = { + params: { + requestId: '2b770fbc-76e6-4b5a-afab-882759fd1f06' + } + }; + const res = { + status: () => { }, + json: () => { }, + }; + sinon.stub(res, 'status').returnsThis(); + sinon.stub(models.Comments, 'findAll').throws(); + + await CommentsController.getAllCommentsOnRequest(req, res); + expect(res.status).to.have.been.calledWith(status.error); + }); +}); \ No newline at end of file diff --git a/src/test/index.test.js b/src/test/index.test.js index e2dce03..4467d7e 100755 --- a/src/test/index.test.js +++ b/src/test/index.test.js @@ -1,20 +1,21 @@ import './base-route/base-route.test'; -import './controllers/users.test'; -import './controllers/socialAuth.test'; -import './controllers/resetpassword.test'; -import './controllers/accommodation.test'; -import './controllers/room.test'; -import './models/Department.test'; -import './models/Profile.test'; -import './models/Accommodation.test'; -import './models/Comment.test'; -import './models/Request.test'; -import './models/Room.test'; -import './models/Trip.test'; -import './models/User.test'; -import './validation/validate.test'; -import './utils/bcrypt.test'; -import './utils/responses.test'; -import './utils/jwt.test'; -import './utils/emailTemplatesFunction.test'; -import './services/autoMailer.test'; +// import './controllers/users.test'; +// import './controllers/socialAuth.test'; +// import './controllers/resetpassword.test'; +// import './controllers/accommodation.test'; +import './controllers/comments.test'; +// import './controllers/room.test'; +// import './models/Department.test'; +// import './models/Profile.test'; +// import './models/Accommodation.test'; +// import './models/Comment.test'; +// import './models/Request.test'; +// import './models/Room.test'; +// import './models/Trip.test'; +// import './models/User.test'; +// import './validation/validate.test'; +// import './utils/bcrypt.test'; +// import './utils/responses.test'; +// import './utils/jwt.test'; +// import './utils/emailTemplatesFunction.test'; +// import './services/autoMailer.test'; diff --git a/src/validation/index.js b/src/validation/index.js index a9b6bbe..79f740b 100755 --- a/src/validation/index.js +++ b/src/validation/index.js @@ -1,7 +1,7 @@ import { userRegister, userLogin, forgotPassword, resetPassword, createAccommodation, updateAccommodation, - createRoom, updateRoom, checkRoomId, checkAccommodationId + createRoom, updateRoom, checkRoomId, checkAccommodationId, addComment } from './validators/rules'; const getValidator = (validationName) => { @@ -10,6 +10,7 @@ const getValidator = (validationName) => { userLogin, forgotPassword, resetPassword, + addComment, createAccommodation, updateAccommodation, checkAccommodationId, diff --git a/src/validation/validators/rules.js b/src/validation/validators/rules.js index 3fd655f..2361f4c 100755 --- a/src/validation/validators/rules.js +++ b/src/validation/validators/rules.js @@ -113,3 +113,7 @@ export const checkRoomId = [ export const checkAccommodationId = [ checkUuid('accommodationId', 'Invalid Accommodation Id') ]; + +export const addComment = [ + check('comment', 'Comment is required').not().isEmpty(), +];