Skip to content

Commit

Permalink
Merge pull request #23 from andela/ft/167190444-user-comment
Browse files Browse the repository at this point in the history
#167190444 User can make comments to articles
  • Loading branch information
topseySuave committed Aug 6, 2019
2 parents 4125a6f + f3dacb9 commit 30859b8
Show file tree
Hide file tree
Showing 15 changed files with 320 additions and 6 deletions.
4 changes: 3 additions & 1 deletion server/controllers/articleController.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ class ArticleController {
if (!article) {
return utils.errorStat(res, 404, 'Article not found');
}
return utils.successStat(res, 200, 'article', article);
let comments = await article.getComment();
comments = Object.values(comments).map(comment => comment.dataValues);
return utils.successStat(res, 200, 'article', { article, comments, noOfComments: comments.length });
}

/**
Expand Down
47 changes: 47 additions & 0 deletions server/controllers/commentController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import models from '../db/models';
import helpers from '../helpers';

const { successStat, errorStat } = helpers;

/**
* @Module CommentController
* @description Controlls comments made by users
*/
export default class CommentController {
/**
* @static
* @param {*} req Request object
* @param {*} res Response object
* @returns {Object} Object containing the user comment, author, and timestaps
* @memberof CommentController
*/
static async addAComment(req, res) {
const { params: { postId }, user } = req;
const { comment } = req.body.comment;
const post = await models.Article.findByPk(postId);
if (!post) return errorStat(res, 404, 'Post not found');
const newComment = {
authorId: user.id,
body: comment,
articleId: postId
};
const userComment = await models.Comment.create(newComment);
await post.addComment(userComment);
const commentResponse = await models.Comment.findOne({
where: { id: userComment.id },
include: [
{
as: 'author',
model: models.User,
attributes: ['id', 'firstname', 'lastname', 'image', 'socialId', 'username']
}
],
attributes: {
exclude: [
'authorId'
]
}
});
return successStat(res, 201, 'comment', commentResponse);
}
}
32 changes: 32 additions & 0 deletions server/db/migrations/20190804084630-create-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Comments', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
body: {
type: Sequelize.TEXT,
allowNull: false
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
},
authorId: {
type: Sequelize.INTEGER,
allowNull: false
},
articleId: {
type: Sequelize.INTEGER,
allowNull: false
}
}),
down: queryInterface => queryInterface.dropTable('Comments')
};
3 changes: 3 additions & 0 deletions server/db/models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ module.exports = (sequelize, DataTypes) => {
});
Article.associate = function (models) {
Article.belongsTo(models.User, { as: 'author', foreignKey: 'authorId', onDelete: 'CASCADE' });
Article.hasMany(models.Comment, {
foreignKey: 'articleId', onDelete: 'CASCADE', as: 'comment', hooks: true
});
};
return Article;
};
13 changes: 13 additions & 0 deletions server/db/models/comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

module.exports = (sequelize, DataTypes) => {
const Comment = sequelize.define('Comment', {
body: DataTypes.TEXT,
authorId: DataTypes.INTEGER,
articleId: DataTypes.INTEGER
}, {});
Comment.associate = (models) => {
Comment.belongsTo(models.Article, { as: 'article' });
Comment.belongsTo(models.User, { as: 'author' });
};
return Comment;
};
1 change: 1 addition & 0 deletions server/db/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = (sequelize, DataTypes) => {
timestamps: false,
});
User.hasMany(models.Article, { foreignKey: 'authorId', onDelete: 'CASCADE' });
User.hasMany(models.Comment, { foreignKey: 'authorId', onDelete: 'CASCADE' });
};
return User;
};
46 changes: 43 additions & 3 deletions server/docs/ah-commando-doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,6 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/errorResponse"



/profiles/{username}:
get:
tags:
Expand Down Expand Up @@ -673,6 +670,49 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/errorResponse"
/comment/{postId}/:
post:
tags:
- Articles
security:
- bearerAuth: []
summary: comment on an article
parameters:
- in: path
name: postId
schema:
type: integer
required: true
description: Id of the post to comment on
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
comment:
type: string
description: An endpoint to make a comment on Articles
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/successResponse"
'401':
description: Authorization error
content:
application/json:
schema:
$ref: "#/components/schemas/errorResponse"
'500':
description: Server error
content:
application/json:
schema:
$ref: "#/components/schemas/errorResponse"
components:
securitySchemes:
bearerAuth:
Expand Down
4 changes: 3 additions & 1 deletion server/middlewares/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const {
validateArticle,
validateProfileUpdate,
validatePasswordReset,
validateEmail
validateEmail,
validateCommentMessage
} = InputValidator;

export default {
Expand All @@ -22,4 +23,5 @@ export default {
validatePasswordReset,
validateEmail,
optionalLogin,
validateCommentMessage
};
16 changes: 15 additions & 1 deletion server/middlewares/inputValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
profileSchema,
articleSchema,
resetPasswordSchema,
resetEmailSchema
resetEmailSchema,
commentBodySchema
} from './schema';
import validate from '../helpers/validate';

Expand Down Expand Up @@ -91,6 +92,19 @@ class InputValidator {
const email = { ...req.body.user };
return validate(email, resetEmailSchema, req, res, next);
}

/**
* @method validateCommentMessage
* @description Validate message input made by user
* @param {object} req - The Request Object
* @param {object} res - The Response Object
* @param {function} next - The next function to point to the next middleware
* @returns {function} validate() - An execucted validate function
*/
static validateCommentMessage(req, res, next) {
const comment = { ...req.body };
return validate(comment, commentBodySchema, req, res, next);
}
}

export default InputValidator;
5 changes: 5 additions & 0 deletions server/middlewares/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,8 @@ export const resetEmailSchema = {
.email({ minDomainSegments: 2 })
.required()
};
export const commentBodySchema = {
comment: Joi.string()
.trim()
.required()
};
16 changes: 16 additions & 0 deletions server/routes/comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import express from 'express';
import middlewares from '../middlewares';
import CommentController from '../controllers/commentController';

const {
verifyToken,
validateCommentMessage
} = middlewares;

const { addAComment } = CommentController;

const commentRouter = express();

commentRouter.post('/:postId', verifyToken, validateCommentMessage, addAComment);

export default commentRouter;
2 changes: 2 additions & 0 deletions server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import userRoute from './user';
import profileRoute from './profile';
import articleRoute from './article';
import imageRoute from './image';
import commentRouter from './comment';
import { cloudinaryConfig } from '../db/config/cloudinaryConfig';


Expand All @@ -24,5 +25,6 @@ router.use('/users', userRoute);
router.use('/articles', articleRoute);
router.use('/image', imageRoute);
router.use('/', profileRoute);
router.use('/comment', commentRouter);

export default router;
105 changes: 105 additions & 0 deletions server/tests/comment.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import app from '../index';
import commentData from './testData/comment.data';
import userData from './testData/user.data';


const { expect } = chai;
chai.use(chaiHttp);
const baseUrl = '/api/v1';

describe('Handle Comment', () => {
let userToken, postId;
before('sign up a user', (done) => {
chai
.request(app)
.post(`${baseUrl}/users/`)
.send(userData[20])
.end(() => {
done();
});
});
describe('Add Comment', () => {
before('sign in a user', (done) => {
chai
.request(app)
.post(`${baseUrl}/users/login`)
.send(userData[21])
.end((err, res) => {
userToken = res.body.user.token;
done();
});
});
before('add a post', (done) => {
chai
.request(app)
.post(`${baseUrl}/articles`)
.set('Authorization', `${userToken}`)
.send({
article: {
title: 'Hello',
description: 'i say hello',
articleBody: 'hhh',
tagList: 'jssd',
image: 'hs.jpg'
}
})
.end((err, res) => {
postId = res.body.articles.id;
done();
});
});
it('Should fail if user isn\'t logged in', (done) => {
chai
.request(app)
.post(`${baseUrl}/comment/${postId}`)
.send(commentData[0])
.end((err, res) => {
const { status, error } = res.body;
expect(status).to.equal(401);
expect(error).to.equal('Authorization error');
done();
});
});
it('Should fail if message is empty', (done) => {
chai
.request(app)
.post(`${baseUrl}/comment/${postId}`)
.set('Authorization', `${userToken}`)
.send(commentData[1])
.end((err, res) => {
const { status, error } = res.body;
expect(status).to.equal(400);
expect(error[0]).to.equal('comment is not allowed to be empty');
done();
});
});
it('Should fail if post is not found', (done) => {
chai
.request(app)
.post(`${baseUrl}/comment/${23}`)
.set('Authorization', `${userToken}`)
.send(commentData[0])
.end((err, res) => {
const { status, error } = res.body;
expect(status).to.equal(404);
expect(error).to.equal('Post not found');
done();
});
});
it('Should pass if user is a valid one and is logged in', (done) => {
chai
.request(app)
.post(`${baseUrl}/comment/${postId}`)
.set('Authorization', `${userToken}`)
.send(commentData[0])
.end((err, res) => {
const { status } = res.body;
expect(status).to.equal(201);
expect(res.body.comment).to.be.an('object');
done();
});
});
});
});
13 changes: 13 additions & 0 deletions server/tests/testData/comment.data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const comment = [
// correct message detail 0
{
comment: 'I love this post, It is very intuitive'
},
// incorrect message details 1
{
comment: ''
}

];

export default comment;
Loading

0 comments on commit 30859b8

Please sign in to comment.