Skip to content

Commit

Permalink
feature-[167190538]: Authenticated user can comment on articles
Browse files Browse the repository at this point in the history
- Add route to add comment to article
- Add route to get all comments for an article (Authentication is optional)
- Add comment validation
- Add tests

[Delivers #167190538]
  • Loading branch information
ayodejiAA committed Aug 16, 2019
1 parent fcec0ee commit 2f240cf
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 79 deletions.
101 changes: 53 additions & 48 deletions server/controllers/Comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ const { Comment, Article, User } = models;
*/
class Comments {
/**
* @name addCommentToArticle
* @name create
* @async
* @static
* @memberof Comments
* @param {Object} req express request object
* @param {Object} res express response object
* @returns {JSON} JSON object with details of new follower
*/
static async addCommentToArticle(req, res) {
static async create(req, res) {
try {
const { id: userId } = req.user;
const { slug } = req.params;
Expand All @@ -28,79 +28,84 @@ class Comments {
if (!article) {
return serverResponse(res, 404, { error: 'article not found' });
}
const articleAuthorId = article.userId;
const isUserFollowingAuthor = await isFollowing(articleAuthorId, userId);
const commentData = await Comment.create({
const articleAuthorId = article.authorId;
let commentData = await Comment.create({
userId,
comment,
articleId: article.id
});

const commentDetail = {
const { dataValues: author } = await commentData.getAuthor({
attributes: ['id', 'userName', 'bio', 'avatarUrl']
});

commentData = {
id: commentData.id,
comment: commentData.comment,
articleId: commentData.articleId,
updatedAt: commentData.updatedAt,
createdAt: commentData.createdAt
};

const { dataValues: user } = await commentData.getUser({
attributes: ['id', 'userName', 'bio', 'avatarUrl']
});

user.following = isUserFollowingAuthor;
commentDetail.user = user;
return serverResponse(res, 201, { comment: commentDetail });
author.following = await isFollowing(articleAuthorId, userId);
commentData.author = author;
return serverResponse(res, 201, { comment: commentData });
} catch (error) {
serverError(res);
}
}

/**
* @name getAllCommentsForArticle
* @name getArticleComments
* @async
* @static
* @memberof Comments
* @param {Object} req express request object
* @param {Object} res express response object
* @returns {JSON} JSON object with details of new follower
*/
static async getAllCommentsForArticle(req, res) {
const { slug } = req.params;
const article = await Article.findBySlug(slug);
if (!article) {
return serverResponse(res, 404, { error: 'article not found' });
}
static async getArticleComments(req, res) {
try {
const { slug } = req.params;
const article = await Article.findBySlug(slug);
if (!article) {
return serverResponse(res, 404, { error: 'article not found' });
}

let comments = await article.getComments({
attributes: { exclude: ['userId'] },
include: [
{
model: User,
as: 'user',
attributes: ['id', 'userName', 'bio', 'avatarUrl']
}
]
});
let comments = await article.getComments({
attributes: { exclude: ['userId'] },
include: [
{
model: User,
as: 'author',
attributes: ['id', 'userName', 'bio', 'avatarUrl']
}
]
});

if (comments.length < 1) {
return serverResponse(res, 404, { error: 'article has no comment' });
}
const articleAuthorId = article.userId;
if (comments.length < 1) {
return serverResponse(res, 200, {
message: 'article has no comment',
comments
});
}
const articleAuthorId = article.authorId;

comments = comments.map(async (comment) => {
const {
user: { id }
} = comment;
const isUserFollowingAuthor = await isFollowing(articleAuthorId, id);
const userDetails = comment.user.dataValues;
userDetails.following = isUserFollowingAuthor;
comment.user = userDetails;
return comment;
});
comments = await Promise.all(comments);
const commentsCount = comments.length;
return serverResponse(res, 200, { comments, commentsCount });
comments = comments.map(async (comment) => {
const {
author: { id }
} = comment;
const isUserFollowingAuthor = await isFollowing(articleAuthorId, id);
const author = comment.author.dataValues;
author.following = isUserFollowingAuthor;
comment.author = author;
return comment;
});
comments = await Promise.all(comments);
const commentsCount = comments.length;
return serverResponse(res, 200, { comments, commentsCount });
} catch (error) {
serverError(res);
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions server/database/models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ export default (sequelize, DataTypes) => {
return article;
};

Article.findBySlug = async (slug) => {
const article = await Article.findOne({ where: { slug } });
if (article) return article;
return null;
};

Article.associate = (models) => {
Article.belongsTo(models.User, {
foreignKey: 'authorId',
Expand Down
2 changes: 1 addition & 1 deletion server/database/models/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default (sequelize, DataTypes) => {
Comment.associate = (models) => {
Comment.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user',
as: 'author',
onDelete: 'CASCADE'
});

Expand Down
12 changes: 9 additions & 3 deletions server/routes/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ import middlewares from '../middlewares';

const route = express.Router();

const { verifyToken, validateCommentBody, getSessionFromToken } = middlewares;
const {
verifyToken,
validateCommentBody,
getSessionFromToken,
checkUserVerification
} = middlewares;

route.post(
'/:slug/comments',
verifyToken,
getSessionFromToken,
checkUserVerification,
validateCommentBody,
Comments.addCommentToArticle
Comments.create
);

route.get('/:slug/comments', Comments.getAllCommentsForArticle);
route.get('/:slug/comments', Comments.getArticleComments);

export default route;
1 change: 1 addition & 0 deletions server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ route.use('/user', userFollower);
route.use('/articles', article);
route.use('/tags', tag);
route.use('/articles', comment);
route.use('/articles', article, comment);

export default route;
10 changes: 9 additions & 1 deletion test/articles/__mocks__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ const newArticleData = {
status: 'publish',
tags: 'technology'
};
const ArticleData10 = {
title: ' is simply dummy text of the printing and typesetting ',
description: 'ext ever since the 1500s, when an unknown printer',
articleBody: 'ext ever since the 1500s, when an unknown printer took a ga',
status: 'publish'
};

const myfile = faker.image.dataUri(200, 200);
const request = {
file: {
Expand Down Expand Up @@ -96,5 +103,6 @@ export {
fakeoutput,
invalidArticleData,
ArticleData4,
badImage
badImage,
ArticleData10
};
102 changes: 76 additions & 26 deletions test/comments/comment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,56 @@ import chai, { expect } from 'chai';
import chaiHttp from 'chai-http';
import sinon from 'sinon';
import { getNewUser } from '../users/__mocks__';
import userData from '../users/__mocks__/user';
import app from '../../server';
import models from '../../server/database/models';
import { ArticleData2, ArticleData10 } from '../articles/__mocks__';

const { Comment } = models;
const { Comment, Article } = models;
const { rightUserWithEmail } = userData;

chai.use(chaiHttp);

const authorUserName = 'JhayXXX';
const artilceSlug = 'rambo-team';
let author;
let artilceSlug;
let artilceSlug2;
let commenter;

const baseUrl = process.env.BASE_URL;

before(async () => {
const user = getNewUser();
const signUpResponse = await chai
.request(app)
.post(`${baseUrl}/users/create`)
.send({
...user,
confirmPassword: user.password
});
commenter = signUpResponse.body;

const loginResponse = await chai
.request(app)
.post(`${baseUrl}/sessions/create`)
.send(rightUserWithEmail);
author = loginResponse.body;

const articleResponse = await chai
.request(app)
.post(`${baseUrl}/articles/create`)
.set('Authorization', author.token)
.send(ArticleData2);
artilceSlug = articleResponse.body.slug;

const articleResponse2 = await chai
.request(app)
.post(`${baseUrl}/articles/create`)
.set('Authorization', author.token)
.send(ArticleData10);
artilceSlug2 = articleResponse2.body.slug;
});

describe('Add Comments to Article Tests', async () => {
before(async () => {
const user = getNewUser();
const res = await chai
.request(app)
.post(`${baseUrl}/users/create`)
.send({
...user,
confirmPassword: user.password
});
commenter = res.body;
});
context('When unregistered user wants to make comment', () => {
it('returns an error', async () => {
const res = await chai
Expand Down Expand Up @@ -82,7 +107,9 @@ describe('Add Comments to Article Tests', async () => {
);

context(
'when authenticated user attempts submitting number of chars above required',
`
when authenticated user attempts submitting number
of chars above required`,
() => {
it('returns error', async () => {
const res = await chai
Expand Down Expand Up @@ -113,29 +140,31 @@ describe('Add Comments to Article Tests', async () => {
});
expect(res).to.have.status(201);
expect(res.body).to.haveOwnProperty('comment');
expect(res.body.comment.user.following).to.be.false; // Not following the author
expect(res.body.comment.author.following).to.be.false;
});
});

context(
'when authenticated user adds comment to following users article',
'when authenticated user adds comment to following user article',
() => {
it("returns comment details with 'following' being true", async () => {
before(async () => {
await chai
.request(app)
.post(`${baseUrl}/profiles/${authorUserName}/follow`)
.post(`${baseUrl}/profiles/${author.user.userName}/follow`)
.set('Authorization', commenter.token);

});
it("returns comment details with 'following' being true", async () => {
const res = await chai
.request(app)
.post(`${baseUrl}/articles/${artilceSlug}/comments`)
.set('authorization', commenter.token)
.send({
comment: new Array(3000).join('a')
});

expect(res).to.have.status(201);
expect(res.body).to.haveOwnProperty('comment');
expect(res.body.comment.user.following).to.be.true; // Following the author
expect(res.body.comment.author.following).to.be.true;
});
}
);
Expand Down Expand Up @@ -179,15 +208,17 @@ describe('Get All Comments for an Article - Test', async () => {
);

context(
'When user attempt fetch all comments for article that does not have comment',
`When user attempt fetch all comments
for article that does not have comment`,
() => {
it('returns an error', async () => {
const res = await chai
.request(app)
.get(`${baseUrl}/articles/creat-sample/comments`);
expect(res).to.have.status(404);
expect(res.body).to.haveOwnProperty('error');
expect(res.body.error).to.deep.equal('article has no comment');
.get(`${baseUrl}/articles/${artilceSlug2}/comments`);
expect(res).to.have.status(200);
expect(res.body).to.haveOwnProperty('message');
expect(res.body).to.haveOwnProperty('comments');
expect(res.body.message).to.deep.equal('article has no comment');
});
}
);
Expand All @@ -202,4 +233,23 @@ describe('Get All Comments for an Article - Test', async () => {
expect(res.body.comments).to.be.an('array');
});
});

context('When there is server error when making a comment', () => {
it('returns a 500 error', async () => {
const stub = sinon.stub(Article, 'findBySlug');
const error = new Error('server error, this will be resolved shortly');
stub.yields(error);

const res = await chai
.request(app)
.get(`${baseUrl}/articles/${artilceSlug}/comments`);

expect(res).to.have.status(500);
expect(res.body).to.include.key('error');
expect(res.body.error).to.equal(
'server error, this will be resolved shortly'
);
stub.restore();
});
});
});

0 comments on commit 2f240cf

Please sign in to comment.