diff --git a/src/controllers/CommentController.js b/src/controllers/CommentController.js new file mode 100644 index 0000000..9c0ba2a --- /dev/null +++ b/src/controllers/CommentController.js @@ -0,0 +1,69 @@ +import { comments, users } from '../database/models'; +import Customize from '../helpers/Customize'; + + +/** + * @exports + * @class CommentController + */ +class CommentController { + /** + * users or managers can comment on a travel request + * @static + * @description POST /api/users/:tripRequestId/comment + * @param {object} req request object + * @param {object} res response object + * @memberof CommentController + * @returns {object} data + */ + static async createComment(req, res) { + try { + const { tripRequestId } = req.params; + const { id } = req.user; + const { comment } = req.body; + const userComment = await comments.create({ + userId: id, + tripRequestId, + comment + + }); + if (userComment) { + return Customize.successMessage(req, res, 'Your comment was posted successfully', comment, 201); + } + } catch (e) { + return Customize.errorMessage(req, res, 'Server error', 500); + } + } + + /** +* users or managers can view comment thread +* @static +* @description POST /api/users/:tripRequestId/comments +* @param {object} req request object +* @param {object} res response object +* @memberof CommentController +* @returns {object} data +*/ + static async getComments(req, res) { + try { + const { tripRequestId } = req.params; + const tripComments = await comments.findAll({ + attributes: ['comment', 'updatedAt'], + include: [{ + model: users, + attributes: ['firstName', 'lastName'] + }], + where: { + tripRequestId, + } + }); + if (tripComments[0]) { + return Customize.successMessage(req, res, 'All comments about this trip request have been retrieved successfuly!', tripComments, 200); + } + return Customize.errorMessage(req, res, 'No comments for this trip yet!', 200); + } catch (e) { + Customize.errorMessage(req, res, 'Server error', 500); + } + } +} +export default CommentController; diff --git a/src/database/migrations/20191119162300-create-comments.js b/src/database/migrations/20191119162300-create-comments.js new file mode 100644 index 0000000..7d74ba7 --- /dev/null +++ b/src/database/migrations/20191119162300-create-comments.js @@ -0,0 +1,46 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('comments', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + comment: { + type: Sequelize.STRING, + allowNull: false + }, + userId: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + references: { + model: 'users', + key: 'id', + } + }, + tripRequestId: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + references: { + model: 'tripRequests', + key: 'id', + } + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('comments'); + } +}; diff --git a/src/database/models/comments.js b/src/database/models/comments.js new file mode 100644 index 0000000..c2b2e24 --- /dev/null +++ b/src/database/models/comments.js @@ -0,0 +1,23 @@ +'use strict'; +module.exports = (sequelize, DataTypes) => { + const comments = sequelize.define('comments', { + comment: DataTypes.STRING, + userId: DataTypes.INTEGER, + tripRequestId: DataTypes.INTEGER, + }, { freezeTableName: true }); + comments.associate = function (models) { + comments.belongsTo(models.users, { + foreignKey: 'userId', + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }); + + comments.belongsTo(models.tripRequests,{ + foreignKey: 'tripRequestId', + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + } + ); + }; + return comments; +}; diff --git a/src/database/seeders/20191115043919-super-admin.js b/src/database/seeders/20191115043919-super-admin.js index c863333..652467b 100644 --- a/src/database/seeders/20191115043919-super-admin.js +++ b/src/database/seeders/20191115043919-super-admin.js @@ -16,9 +16,9 @@ module.exports = { updatedAt: new Date() }, { - firstName: 'Brian', - lastName: 'Maiyo', - email: 'maiyo.brian@andela.com', + firstName: 'mailer', + lastName: 'manager', + email: 'manager.email@gmail.com', password: '$2b$10$/Zh5Kwn95d/LPBwjL2VRc.Hpef/XZ2spe6U0eLksDgcxDuroB4XSq', isVerified: true, signupType: 'Barefoot', diff --git a/src/helpers/ControllerHelper.js b/src/helpers/ControllerHelper.js index 9837fce..7d6767d 100644 --- a/src/helpers/ControllerHelper.js +++ b/src/helpers/ControllerHelper.js @@ -26,10 +26,9 @@ class ControllerHelper { const newTrip = await tripRequests.create({ userId, statusId: 1, tripTypeId }); - const request = await tripRequests.findOne({ where: { userId } }); itinerary.forEach(async (item) => { await trips.create({ - tripRequestId: request.dataValues.id, + tripRequestId: newTrip.dataValues.id, originId: item.originId, destinationId: item.destinationId, reason: item.reason, diff --git a/src/middlewares/Exists.js b/src/middlewares/Exists.js index d392c59..74168df 100644 --- a/src/middlewares/Exists.js +++ b/src/middlewares/Exists.js @@ -25,6 +25,26 @@ class Exists { 'The trip is either approved rejected or it doesn\'t exist' ); } + + /** +* check is the request exist +* @static +* @param {object} req request object +* @param {object} res response object +* @param {object} next next +* @memberof Exists +* @returns {object} data +*/ + static isTripRequestExist(req, res, next) { + return DataEngine.findOne( + req, + res, + next, + tripRequests, + { id: req.params.tripRequestId }, + 'The trip request doen\'t exist' + ); + } } diff --git a/src/middlewares/Validate.js b/src/middlewares/Validate.js index 2995eb3..d99d174 100644 --- a/src/middlewares/Validate.js +++ b/src/middlewares/Validate.js @@ -145,5 +145,28 @@ class Validate { check('bio', 'Please your bio is needed to complete your profile(at least 15 characters)').isLength({ min: 15 }) ]; } + + /** +* Validate user comment post +* @static +* @returns {object} errors +*/ + static commentPostRules() { + return [ + check('comment', 'Comment should be of at least two characters').isLength({ min: 2 }), + check('tripRequestId', 'Trip Request Id shopuld be of integer').isInt() + ]; + } + + /** +* Validate user view comments +* @static +* @returns {object} errors +*/ + static getCommentsRules() { + return [ + check('tripRequestId', 'Trip Request Id shopuld be of integer type').isInt() + ]; + } } export default Validate; diff --git a/src/middlewares/findUsers.js b/src/middlewares/findUsers.js index 3815483..a887bc2 100644 --- a/src/middlewares/findUsers.js +++ b/src/middlewares/findUsers.js @@ -1,4 +1,5 @@ -import { users } from '../database/models'; +import { users, tripRequests } from '../database/models'; +import Customize from '../helpers/Customize'; const findOneUser = async (req, res, next) => { const { email } = req.body; @@ -9,4 +10,24 @@ const findOneUser = async (req, res, next) => { } next(); }; +export const commentAccess = async (req, res, next) => { + const { id } = req.user; + const { tripRequestId } = req.params; + const isManager = await users.findOne({ + where: { + id, + roleId: 6, + } + }); + const isRequester = await tripRequests.findOne({ + where: { + id: tripRequestId, + userId: id, + } + }); + if (isManager || isRequester) { + return next(); + } + Customize.errorMessage(req, res, 'You should be either a requester or a manager', 403); +}; export default findOneUser; diff --git a/src/middlewares/isManager.js b/src/middlewares/isManager.js index 4285037..ca17ade 100644 --- a/src/middlewares/isManager.js +++ b/src/middlewares/isManager.js @@ -10,8 +10,9 @@ const isManager = async (req, res, next) => { } }); - if (!manager) { Customize.errorMessage(req, res, 'Unknown line manager', 404); } - next(); + if (manager) { return next(); } + + Customize.errorMessage(req, res, 'Unknown line manager', 404); }; export default isManager; diff --git a/src/routes/api/commentRoute.js b/src/routes/api/commentRoute.js new file mode 100644 index 0000000..f5f63ec --- /dev/null +++ b/src/routes/api/commentRoute.js @@ -0,0 +1,113 @@ +import express from 'express'; +import Validate from '../../middlewares/Validate'; +import checkInputDataError from '../../middlewares/checkInputDataError'; +import { commentAccess } from '../../middlewares/findUsers'; +import Exists from '../../middlewares/Exists'; +import isUserVerified from '../../middlewares/isUserVerified'; +import AuthenticateToken from '../../helpers/AuthenticateToken'; +import CommentController from '../../controllers/CommentController'; + +const commentRoute = express.Router(); + +/** +* @swagger +* +* /trips/{tripRequestId}/comment: +* post: +* summary: User or manager can post a comment +* description: user/manager post comment +* tags: +* - Comment +* parameters: +* - name: token +* in: header +* required: true +* description: user token +* schema: +* type: string +* example: xxxxxxxxx.xxxxxxxxx.xxxxxxxxxx +* minimum: 1 +* - name: tripRequestId +* in: path +* required: true +* description: Trip request id +* schema: +* type: integer +* example: 1 +* minimum: 1 +* requestBody: +* content: +* application/json: +* schema: +* type: object +* properties: +* comment: +* type: string +* responses: +* 201: +* description: Comment posted successfully +* 400: +* description: Unable to post comment +* 401: +* description: Unauthorized +* 403: +* description: Only requesters and managers can post comments +* 500: +* description: Internal server error +*/ +commentRoute.post('/:tripRequestId/comment', + AuthenticateToken.verifyToken, + isUserVerified, + Validate.commentPostRules(), + checkInputDataError, + Exists.isTripRequestExist, + commentAccess, + CommentController.createComment); + + +/** +* @swagger +* +* /trips/{tripRequestId}/comments: +* get: +* summary: User or manager can get a comment +* description: user/manager get comment +* tags: +* - Comment +* parameters: +* - name: token +* in: header +* required: true +* description: user token +* schema: +* type: string +* example: xxxxxxxxx.xxxxxxxxx.xxxxxxxxxx +* minimum: 1 +* - name: tripRequestId +* in: path +* required: true +* description: Trip request id +* schema: +* type: integer +* example: 1 +* minimum: 1 +* responses: +* 200: +* description: Comments retrieved successfully +* 401: +* description: Unauthorized +* 403: +* description: Only requesters and managers can get comments +* 500: +* description: Internal server error +*/ +commentRoute.get('/:tripRequestId/comments', + AuthenticateToken.verifyToken, + isUserVerified, + Validate.getCommentsRules(), + checkInputDataError, + Exists.isTripRequestExist, + commentAccess, + CommentController.getComments); + +export default commentRoute; diff --git a/src/routes/api/index.js b/src/routes/api/index.js index b715390..bb93d8d 100755 --- a/src/routes/api/index.js +++ b/src/routes/api/index.js @@ -5,11 +5,13 @@ import userRoute from './userRoute'; import socialRoute from './socialRoute'; import roleRoute from './roleRoute'; import locationRoute from './locationRoute'; +import commentRoute from './commentRoute'; const router = Router(); router.use('/auth', authRoute); router.use('/auth', socialRoute); router.use('/trips', tripRoute); +router.use('/trips', commentRoute); router.use('/users', roleRoute); router.use('/location', locationRoute); router.use('/users', userRoute); diff --git a/src/tests/010-userTest.js b/src/tests/010-userTest.js index b09f261..3803320 100644 --- a/src/tests/010-userTest.js +++ b/src/tests/010-userTest.js @@ -223,7 +223,7 @@ describe('User profile page settings', () => { done(err); }); }); - it('it should not update user profile with invalid filed', (done) => { + it('it should not update user profile with invalid image', (done) => { chai.request(app) .put('/api/v1/users/profile-settings') .set('token', unverifiedUserToken) @@ -245,7 +245,7 @@ describe('User profile page settings', () => { done(err); }); }); - it('it should not update profile of unidentified user', (done) => { + it('it should update user profile successfully', (done) => { chai.request(app) .put('/api/v1/users/profile-settings') .set('token', verifiedUserToken) diff --git a/src/tests/080-commentTest.js b/src/tests/080-commentTest.js new file mode 100644 index 0000000..4902479 --- /dev/null +++ b/src/tests/080-commentTest.js @@ -0,0 +1,131 @@ +import chai from 'chai'; +import app from '../index'; +import mockData from './mock/mockData'; +import tripMockData from './mock/tripMockData'; + +const { expect } = chai; + +let trueToken, userToken, superToken, tripId; +describe('User/manager should be able to post/get comments', () => { + before((done) => { + chai.request(app) + .post('/api/v1/auth/signin') + .send(mockData.user2) + .end((err, res) => { + trueToken = res.body.data; + done(err); + }); + }); + before((done) => { + chai.request(app) + .post('/api/v1/auth/signin') + .send(mockData.newSuperAdminLogin) + .end((err, res) => { + superToken = res.body.data; + done(err); + }); + }); + before((done) => { + chai.request(app) + .post('/api/v1/trips/oneway') + .set('token', superToken) + .send(tripMockData.oneWaytrip) + .end((err, res) => { + tripId = res.body.data.id; + done(err); + }); + }); + before((done) => { + chai.request(app) + .post('/api/v1/auth/signin') + .send(mockData.user1) + .end((err, res) => { + userToken = res.body.data; + done(err); + }); + }); + it('It should post a comment successfully', (done) => { + chai.request(app) + .post('/api/v1/trips/1/comment') + .set('token', trueToken) + .send(mockData.userComment) + .end((err, res) => { + expect(res.status).eql(201); + expect(res.body).to.be.an('object'); + done(err); + }); + }); + it('It should not post empty comment', (done) => { + chai.request(app) + .post('/api/v1/trips/1/comment') + .set('token', trueToken) + .end((err, res) => { + expect(res.status).eql(400); + expect(res.body.message[0]).eql('Comment should be of at least two characters'); + done(err); + }); + }); + it('It should not post empty comment unless the commenter is requester/manager', (done) => { + chai.request(app) + .post('/api/v1/trips/1/comment') + .set('token', userToken) + .send(mockData.userComment) + .end((err, res) => { + expect(res.status).eql(403); + expect(res.body.message).eql('You should be either a requester or a manager'); + done(err); + }); + }); + it('It should not post comment with invalid trip Id', (done) => { + chai.request(app) + .post('/api/v1/trips/d/comment') + .set('token', trueToken) + .send(mockData.userComment) + .end((err, res) => { + expect(res.status).eql(400); + expect(res.body.message[0]).eql('Trip Request Id shopuld be of integer'); + done(err); + }); + }); + it('It should get comments of a specific trip successfully', (done) => { + chai.request(app) + .get('/api/v1/trips/1/comments') + .set('token', trueToken) + .end((err, res) => { + expect(res.status).eql(200); + expect(res.body).to.be.an('object'); + done(err); + }); + }); + it('It should get 0 comments of a specific trip', (done) => { + chai.request(app) + .get(`/api/v1/trips/${tripId}/comments`) + .set('token', superToken) + .end((err, res) => { + expect(res.status).eql(200); + expect(res.body.message).eql('No comments for this trip yet!'); + done(err); + }); + }); + it('It should not get comment unless the commenter is requester/manager', (done) => { + chai.request(app) + .get(`/api/v1/trips/${tripId}/comments`) + .set('token', userToken) + .end((err, res) => { + expect(res.status).eql(403); + expect(res.body.message).eql('You should be either a requester or a manager'); + done(err); + }); + }); + it('It should not get comment with invalid trip Id', (done) => { + chai.request(app) + .get('/api/v1/trips/d/comments') + .set('token', trueToken) + .send(mockData.userComment) + .end((err, res) => { + expect(res.status).eql(400); + expect(res.body.message[0]).eql('Trip Request Id shopuld be of integer type'); + done(err); + }); + }); +}); diff --git a/src/tests/mock/mockData.js b/src/tests/mock/mockData.js index f8f5723..3588f4a 100644 --- a/src/tests/mock/mockData.js +++ b/src/tests/mock/mockData.js @@ -56,6 +56,14 @@ const mockData = { id: 1, email: 'demo@gmail.com' }, + user1: { + email: 'jean@gmail.com', + password: 'admin1224' + }, + user2: { + email: 'manager.email@gmail.com', + password: 'admin1234' + }, wrongEmailFormat: { email: 'demo.com', }, @@ -165,7 +173,9 @@ const mockData = { department: 'IT', managerId: 5, bio: 'I have been a christian since 2014, Christ as my savior' - } + }, + userComment: + { comment: 'Everyone in the company should request for this trip' } }; export const superAdminToken = AuthenticateToken.signToken(mockData.newSuperAdmin); diff --git a/src/utils/swaggerOptions.js b/src/utils/swaggerOptions.js index ebf49c6..710f846 100644 --- a/src/utils/swaggerOptions.js +++ b/src/utils/swaggerOptions.js @@ -45,7 +45,8 @@ const options = { path.resolve(__dirname, '../routes/api/tripRoute.js'), path.resolve(__dirname, '../routes/api/roleRoute.js'), path.resolve(__dirname, '../routes/api/locationRoute.js'), - path.resolve(__dirname, '../routes/api/userRoute.js') + path.resolve(__dirname, '../routes/api/userRoute.js'), + path.resolve(__dirname, '../routes/api/commentRoute.js') ] }; export default options;