Skip to content

Commit

Permalink
166816213-feature(article, Article, ArticleLikes, User, helpers, utils)
Browse files Browse the repository at this point in the history
- Add model and migration for ArticleLike
- Add ArticleLike model instance and class methods
- Add validation for like and dislike endpoint
- Add likeArticle and dislikeArticle method in Article controller
- Add route for like and dislike article
- Add integration tests for the endpoint
- Document endpoint

[Delivers #166816213]
  • Loading branch information
fantastic-genius committed Jul 11, 2019
1 parent b6d9bdf commit f1df15f
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 3 deletions.
65 changes: 64 additions & 1 deletion controllers/articles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,68 @@ export default {
error: e.message,
});
}
}
},

voteArticle: async (req, res) => {
const { slug } = req.params;
let { status } = req.body;
status = status === 'true' || status === 'false' ? JSON.parse(status) : status;
const article = await db.Article.findOne({
where: {
slug
}
});

try {
if (!article) {
return res.status(404).json({
error: 'This article does not exist'
});
}

const articleId = article.id;
const userId = req.user.id;
const voteDetails = {
userId,
articleId,
status
};

const vote = await db.ArticleVote.findArticleVote(voteDetails);

let resStatus = 201;
let message = status ? 'You upvote this article' : 'You downvote this article';


if (!vote) {
await db.ArticleVote.create(voteDetails);
} else {
switch (status) {
case true:
case false:
await vote.updateArticleVote(status);
resStatus = 200;
break;
default:
await vote.deleteArticleVote();
message = 'You have unvote this article';
resStatus = 200;
}
}

const upvotes = await db.ArticleVote.getArticleVotes({ ...voteDetails, status: true });
const downvotes = await db.ArticleVote.getArticleVotes({ ...voteDetails, status: false });

return res.status(resStatus).json({
message,
upvotes,
downvotes
});
} catch (e) {
/* istanbul ignore next */
return res.status(500).json({
error: e.message,
});
}
},
};
42 changes: 42 additions & 0 deletions db/migrations/20190708142539-create-article-votes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('ArticleVotes', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
required: true,
references: {
model: 'Users',
key: 'id',
as: 'user',
},
},
articleId: {
type: Sequelize.INTEGER,
required: true,
references: {
model: 'Articles',
key: 'id',
as: 'article',
},
},
status: {
allowNull: false,
type: Sequelize.BOOLEAN,
required: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('ArticleVotes')
};
7 changes: 7 additions & 0 deletions db/models/Article.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ module.exports = (sequelize, DataTypes) => {
foreignKey: 'userId',
as: 'author',
});

Article.hasMany(models.ArticleVote, {
foreignKey: 'articleId',
as: 'articleVote',
cascade: true
});
};

return Article;
};
47 changes: 47 additions & 0 deletions db/models/ArticleVote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module.exports = (sequelize, DataTypes) => {
const ArticleVote = sequelize.define('ArticleVote', {
status: DataTypes.BOOLEAN
}, {});
ArticleVote.associate = (models) => {
ArticleVote.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
});
ArticleVote.belongsTo(models.Article, {
foreignKey: 'articleId',
as: 'article'
});
};

ArticleVote.getArticleVotes = (data) => {
const { articleId, status } = data;
return ArticleVote.count({
where: {
articleId,
status
}
});
};

ArticleVote.findArticleVote = (data) => {
const { userId, articleId } = data;
return ArticleVote.findOne({
where: {
userId,
articleId
}
});
};

ArticleVote.prototype.updateArticleVote = function update(status) {
return this.update({
status
});
};

ArticleVote.prototype.deleteArticleVote = function deleteVote() {
return this.destroy();
};

return ArticleVote;
};
9 changes: 8 additions & 1 deletion db/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ module.exports = (sequelize, DataTypes) => {
foreignKey: 'userId',
as: 'article',
cascade: true,
}),
});

User.hasMany(models.Ratings, {
foreignKey: 'userId',
as: 'rate',
cascade: true,
});

User.hasMany(models.ArticleVote, {
foreignKey: 'userId',
as: 'articleVote',
cascade: true
});
};

User.prototype.passwordsMatch = function match(password) {
Expand Down
2 changes: 2 additions & 0 deletions routes/v1/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ router.delete(
Article.deleteArticle,
);

router.post('/vote/:slug', Middleware.authenticate, Middleware.isblackListedToken, Article.voteArticle);

export default router;
29 changes: 29 additions & 0 deletions swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,35 @@ paths:
description: "User not Authorized to update article"
404:
description: "Article slug not found"
/articles/vote/{slug}:
post:
tags:
- "Articles"
summary: Vote an Article
description: Vote an article
operationId: voteArticle
produces:
- application/json
parameters:
- name: slug
in: path
description: Slug of article to vote
required: true
type: string
- name: x-access-token
in: header
description: Authorization token
required: true
type: string
responses:
200:
description: You have unvote this article
201:
description: You upote this article
400:
description: Pass in the appropriate status (true or false)
404:
description: This article does not exist
/profiles/{username}:
get:
tags:
Expand Down
3 changes: 3 additions & 0 deletions tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ export const createUser = async (user) => {
};

export const createArticle = async article => db.Article.create(article);

export const createRate = async rating => db.Ratings.create(rating);

export const createArticleVote = async vote => db.ArticleVote.create(vote);
87 changes: 86 additions & 1 deletion tests/routes/articles.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chai from 'chai';
import sinon from 'sinon';
import chaiHttp from 'chai-http';
import { app, db } from '../../server';
import { createUser, createArticle, createRate } from '../helpers';
import { createUser, createArticle, createRate, createArticleVote } from '../helpers';
import * as utils from '../../utils';

const { expect } = chai;
Expand Down Expand Up @@ -316,6 +316,7 @@ describe('ARTICLES TEST', () => {
expect(res.body.message).to.include('Article not found');
});
});

describe('Rate articles', () => {
let userToken;
let articleData;
Expand Down Expand Up @@ -406,4 +407,88 @@ describe('ARTICLES TEST', () => {
expect(res.body.message).to.be.equal('Article does not exist');
});
});

describe('Vote and downvote Article', () => {
let userToken;
let upvote;
let articleSlug;

beforeEach(async () => {
await db.ArticleVote.destroy({ truncate: true, cascade: true });
const user = await createUser({ ...register, email: 'man@havens.com' });
const userResponse = user.response();
const { token } = userResponse;
userToken = token;
const newArticle = await createArticle({ ...article, userId: userResponse.id });
articleSlug = newArticle.slug;

upvote = {
userId: userResponse.id,
articleId: newArticle.id,
status: true
};
});

it('Should upvote an article', async () => {
const res = await chai
.request(app)
.post(`/api/v1/articles/vote/${articleSlug}`)
.set('x-access-token', userToken)
.send({
status: true
});
expect(res.status).to.equal(201);
expect(res.body).to.be.an('object');
expect(res.body.message).to.equal('You upvote this article');
});

it('Should change upvote to downvote', async () => {
await createArticleVote(upvote);
const res = await chai
.request(app)
.post(`/api/v1/articles/vote/${articleSlug}`)
.set('x-access-token', userToken)
.send({
status: false
});
expect(res.status).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.message).to.equal('You downvote this article');
});

it('Should unvote an article', async () => {
await createArticleVote(upvote);
const res = await chai
.request(app)
.post(`/api/v1/articles/vote/${articleSlug}`)
.set('x-access-token', userToken);
expect(res.status).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.message).to.equal('You have unvote this article');
});

it('Should downvote an article', async () => {
const res = await chai
.request(app)
.post(`/api/v1/articles/vote/${articleSlug}`)
.set('x-access-token', userToken)
.send({
status: false
});
expect(res.status).to.equal(201);
expect(res.body).to.be.an('object');
expect(res.body.message).to.equal('You downvote this article');
});

it('Should return error message when article does not exist', async () => {
const wrongSlug = `${articleSlug}-1234`;
const res = await chai
.request(app)
.post(`/api/v1/articles/vote/${wrongSlug}`)
.set('x-access-token', userToken);
expect(res.status).to.equal(404);
expect(res.body).to.be.an('object');
expect(res.body.error).to.equal('This article does not exist');
});
});
});

0 comments on commit f1df15f

Please sign in to comment.