diff --git a/src/controllers/accommodationController.js b/src/controllers/accommodationController.js index cef00a5..d144080 100644 --- a/src/controllers/accommodationController.js +++ b/src/controllers/accommodationController.js @@ -1,3 +1,4 @@ +/* eslint-disable no-unused-expressions */ import models from '../models'; import { successResponse, errorResponse } from '../utils'; diff --git a/src/controllers/commentsController.js b/src/controllers/commentsController.js new file mode 100644 index 0000000..456b4c7 --- /dev/null +++ b/src/controllers/commentsController.js @@ -0,0 +1,132 @@ +import models from '../models'; +import { successResponse, errorResponse, status } from '../utils'; + +const { Comments, Requests, Users } = models; + +const association = [ + { + model: Users, + as: 'theUser', + attributes: ['id', 'firstName', 'lastName', 'email'] + } +]; + +/** + * @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.findOne({ where: { id: commentId }, include: association }); + 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 }, include: association }); + 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..8e116f8 100755 --- a/src/models/Comment.js +++ b/src/models/Comment.js @@ -6,7 +6,8 @@ export default (sequelize, DataTypes) => { }, }, {}); Comment.associate = (models) => { - Comment.belongsTo(models.Users, { as: 'theComment', foreignKey: 'userId' }); + Comment.belongsTo(models.Users, { as: 'theUser', foreignKey: 'userId' }); + Comment.belongsTo(models.Requests, { as: 'theRequest', foreignKey: 'requestId' }); }; return Comment; }; diff --git a/src/routes/api/comments.js b/src/routes/api/comments.js new file mode 100644 index 0000000..9db7953 --- /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.put('/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 5d01c1e..b0e6334 100755 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -5,6 +5,8 @@ import profileRoutes from './api/profile'; import resetPasswordRoute from './api/resetPassword'; import accommodationRoute from './api/accommodation'; import roomRoute from './api/room'; +import commentsRoute from './api/comments'; + const router = new Router(); @@ -14,5 +16,7 @@ router.use('/', roomRoute); router.use('/auth', authRoutes); router.use('/users', userRoute); router.use('/profiles', profileRoutes); +router.use('/', commentsRoute); + export default router; diff --git a/src/services/index.js b/src/services/index.js index 76fddf1..e193347 100755 --- a/src/services/index.js +++ b/src/services/index.js @@ -1,3 +1,4 @@ +/* eslint-disable import/prefer-default-export */ import sendEmail from './autoMailer'; export { sendEmail }; diff --git a/src/test/controllers/comments.test.js b/src/test/controllers/comments.test.js new file mode 100644 index 0000000..9f57f2b --- /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 if comment is not found', (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, 'findOne').throws(); + + await CommentsController.getCommentById(req, res); + expect(res.status).to.have.been.calledWith(status.error); + }); +}); + +// UPDATE Single comment Testing +describe('/PUT 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) + .put('/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) + .put('/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) + .put('/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 comment is not found', (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/b356097c-c6d0-4a3d-85f6-33bc2595c984') + .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: '777f640e-a2ff-45ee-9ce1-bf37645c42d6' + } + }; + 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 when request is not found', (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/777f640e-a2ff-45ee-9ce1-bf37645c42d8/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 5b0c891..cd3195e 100755 --- a/src/test/index.test.js +++ b/src/test/index.test.js @@ -3,6 +3,7 @@ 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 './controllers/profile.test'; import './models/Department.test'; diff --git a/src/validation/index.js b/src/validation/index.js index 257366d..e5eba37 100755 --- a/src/validation/index.js +++ b/src/validation/index.js @@ -1,8 +1,7 @@ import { userRegister, userLogin, forgotPassword, resetPassword, createAccommodation, updateAccommodation, - createRoom, updateRoom, checkRoomId, checkAccommodationId, - checkUserId + createRoom, updateRoom, checkRoomId, checkAccommodationId, checkUserId, addComment } from './validators/rules'; const getValidator = (validationName) => { @@ -11,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 7c92165..f700eed 100755 --- a/src/validation/validators/rules.js +++ b/src/validation/validators/rules.js @@ -118,3 +118,7 @@ export const checkRoomId = [ export const checkAccommodationId = [ checkUuid('accommodationId', 'Invalid Accommodation Id') ]; + +export const addComment = [ + check('comment', 'Comment is required').not().isEmpty(), +];