diff --git a/.gitignore b/.gitignore index bf699bb..ff62c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,9 @@ typings/ #ignore DS_Store .DS_Store +#ignore DS_Store +.DS_Store + # ignore package-lock package-lock.json diff --git a/README.md b/README.md index c720580..ce0cf1e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Reviewed by Hound CI](https://img.shields.io/badge/Reviewed%20by-Hound%20CI-blue.svg)](https://houndci.com) Authors Haven - A Social platform for the creative at heart. -======= ## Vision diff --git a/app.json b/app.json index f023cff..67be33f 100644 --- a/app.json +++ b/app.json @@ -1,54 +1,54 @@ { - "name": "codepirates-ah-backend", - "scripts": {}, - "env": { - "ADMIN_EMAIL": { - "required": true - }, - "ADMIN_PASSWORD": { - "required": true - }, - "API_VERSION": { - "required": true - }, - "DATABASE_URL": { - "required": true - }, - "DB_HOST": { - "required": true - }, - "DB_NAME": { - "required": true - }, - "DB_PASSWORD": { - "required": true - }, - "DB_PORT": { - "required": true - }, - "DB_USER": { - "required": true - }, - "SECRET": { - "required": true - }, - "STATE": { - "required": true - }, - "USER_PASSWORD": { - "required": true - } - }, - "formation": { - "web": { - "quantity": 1 - } - }, - "addons": [], - "buildpacks": [ - { - "url": "heroku/nodejs" - } - ], - "stack": "heroku-18" - } \ No newline at end of file + "name": "codepirates-ah-backend", + "scripts": {}, + "env": { + "ADMIN_EMAIL": { + "required": true + }, + "ADMIN_PASSWORD": { + "required": true + }, + "API_VERSION": { + "required": true + }, + "DATABASE_URL": { + "required": true + }, + "DB_HOST": { + "required": true + }, + "DB_NAME": { + "required": true + }, + "DB_PASSWORD": { + "required": true + }, + "DB_PORT": { + "required": true + }, + "DB_USER": { + "required": true + }, + "SECRET": { + "required": true + }, + "STATE": { + "required": true + }, + "USER_PASSWORD": { + "required": true + } + }, + "formation": { + "web": { + "quantity": 1 + } + }, + "addons": [], + "buildpacks": [ + { + "url": "heroku/nodejs" + } + ], + "stack": "heroku-18" +} diff --git a/package.json b/package.json index 8d44d66..b274543 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "undoseeds": "babel-node node_modules/.bin/sequelize db:seed:undo:all", "migration": "babel-node node_modules/.bin/sequelize db:migrate", "undomigration": "babel-node node_modules/.bin/sequelize db:migrate:undo:all", - "runmigrations": "npm run undoseeds && npm run migration && npm run seeds", + "runmigrations": " npm run undomigration && npm run migration && npm run seeds", "coveralls": "nyc report --reporter=text-lcov | coveralls", "test": "export NODE_ENV=test && npm run undomigration && npm run migration && npm run seeds && nyc --reporter=html --reporter=text mocha ./test --exit --require @babel/register", "dev": "nodemon --exec babel-node ./src/app.js" diff --git a/src/app.js b/src/app.js index e0c65c2..d7350af 100644 --- a/src/app.js +++ b/src/app.js @@ -5,6 +5,7 @@ import routes from './routes/index'; import './config/cloudinary.config'; import { mock } from './middlewares/validators/socialLogin-mock'; + const app = express(); if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') { diff --git a/src/controllers/articles.controller.js b/src/controllers/articles.controller.js index ffbd8eb..bed7bf0 100644 --- a/src/controllers/articles.controller.js +++ b/src/controllers/articles.controller.js @@ -10,7 +10,7 @@ import NotificationServices from '../services/notification.service'; import cloudinaryHelper from '../helpers/cloudinaryHelper'; import OpenUrlHelper from '../helpers/share.article.helper'; import Util from '../helpers/util'; - +import statsService from '../services/db.service'; const { notifyViaEmailAndPush } = NotificationServices; const util = new Util(); @@ -123,6 +123,12 @@ class Articles { const article = _.pick(findArticle, ['slug', 'title', 'description', 'body', 'taglist', 'favorited', 'favoritedcount', 'flagged', 'images', 'views']); const readTime = Helper.calculateReadTime(article.body); article.readtime = readTime; + if (req.auth) { + const { description } = article; + const readerId = req.auth.id; + const item = 'article'; + await statsService.createStat({ description, item, readerId }, 'Stats'); + } return res.status(200).json({ status: 200, message: 'Article successfully retrieved', @@ -223,15 +229,15 @@ class Articles { switch (req.params.channel) { case 'facebook': await OpenUrlHelper.openUrl(`https:www.facebook.com/sharer/sharer.php?u=${url}`); - util.setError(200, `Article shared to ${req.params.channel}`); + util.setSuccess(200, `Article shared to ${req.params.channel}`, url); return util.send(res); case 'twitter': await OpenUrlHelper.openUrl(`https://twitter.com/intent/tweet?url=${url}`); - util.setError(200, `Article shared to ${req.params.channel}`); + util.setSuccess(200, `Article shared to ${req.params.channel}`, url); return util.send(res); case 'mail': await OpenUrlHelper.openUrl(`mailto:?subject=${article.title}&body=${url}`); - util.setError(200, `Article shared to ${req.params.channel}`); + util.setSuccess(200, `Article shared to ${req.params.channel}`, url); return util.send(res); default: break; diff --git a/src/controllers/comments.controller.js b/src/controllers/comments.controller.js index 9649ae0..b7dc2e5 100644 --- a/src/controllers/comments.controller.js +++ b/src/controllers/comments.controller.js @@ -1,8 +1,11 @@ import commentsService from '../services/comments.service'; import UserService from '../services/user.service'; import models from '../models'; +import Helper from '../helpers/helper'; import NotificationServices from '../services/notification.service'; import Util from '../helpers/util'; +import StatsService from '../services/db.service'; + const util = new Util(); @@ -107,6 +110,9 @@ class Comments { await util.setError(200, 'No comments found'); return util.send(res); } + const readerId = req.auth.id; + const item = 'comment'; + await StatsService.createStat({ readerId, item, slug: 'all comments' }, 'Stats'); await util.setSuccess(200, 'All comments successfully retrieved', comments); return util.send(res); } @@ -140,5 +146,104 @@ class Comments { await util.setSuccess(200, 'Update is successfully', updateComment); return util.send(res); } + + /** + * + * + * @static + * @param {*} req + * @param {*} res + * @returns {Object} return json object + * @memberof Comments + */ + static async likeComment(req, res) { + const { id } = req.params; + const { username } = req.auth; + const comment = await commentsService.findOne(id); + + if (!comment) { + util.setError(404, `Comment with id: ${id} does not exist.`); + return util.send(res); + } + let { likesCount, likeInfo } = comment; + const userHasLikedBefore = likeInfo.search(username); + if (userHasLikedBefore >= 0) { + util.setError(400, 'You liked this comment already'); + return util.send(res); + } + + likesCount += 1; + likeInfo = `${username}, ${likeInfo}`; + await commentsService.updateComment(id, { likesCount, likeInfo }); + + const formattedLikeInfo = Helper.formatLikeInfo(likeInfo); + util.setSuccess(201, { likesCount, formattedLikeInfo }); + return util.send(res); + } + + /** + * + * + * @static + * @param {*} req + * @param {*} res + * @returns {Object} return json object + * @memberof Comments + */ + static async updateLikeComment(req, res) { + const { id } = req.params; + const { username } = req.auth; + const comment = await commentsService.findOne(id); + if (!comment) { + util.setError(404, `Comment with id: ${id} does not exist.`); + return util.send(res); + } + + let { likesCount, likeInfo } = comment; + const userHasLikedBefore = likeInfo.search(username); + + if (userHasLikedBefore === -1) { + util.setError(400, 'You did not like this comment before'); + return util.send(res); + } + + likesCount -= 1; + likeInfo = likeInfo.replace(`${username}, `, ''); + await commentsService.updateComment(id, { likesCount, likeInfo }); + + util.setSuccess(200, 'You unliked this comment successfully'); + return util.send(res); + } + + /** + * + * + * @static + * @param {*} req + * @param {*} res + * @returns {Object} return json object + * @memberof Comments + */ + static async getLikesComments(req, res) { + const { id } = req.params; + const { username } = req.auth; + const comment = await commentsService.findOne(id); + + if (!comment) { + util.setError(404, `Comment with id: ${id} does not exist.`); + return util.send(res); + } + const { likesCount, likeInfo } = comment; + + const userHasLikedBefore = likeInfo.search(username); + + if (userHasLikedBefore === -1) { + util.setSuccess(200, 'Likes successfully retrieved', { data: { likesCount, likeInfo } }); + return util.send(res); + } + const formattedLikeInfo = Helper.formatLikeInfo(likeInfo.replace(`${username}, `, '')); + util.setSuccess(200, { data: { likesCount, formattedLikeInfo } }); + return util.send(res); + } } export default Comments; diff --git a/src/controllers/rating.controller.js b/src/controllers/rating.controller.js index a0f9b2c..22def46 100644 --- a/src/controllers/rating.controller.js +++ b/src/controllers/rating.controller.js @@ -1,5 +1,10 @@ -import db from '../models/index'; +import db from '../models'; import RateService from '../services/rate.service'; +import Util from '../helpers/util'; + +// const db = models.Rate; + +const util = new Util(); /** * * @@ -15,72 +20,57 @@ class rateController { * @returns {Object} return rating information to user * @memberof UserController */ - static async setArticleRating(req, res) { + static async createOrUpdateRate(req, res) { try { // Initialize rating data const userEmail = req.auth.email; const { rate } = req.body; const { articleSlug } = req.params; const rateSchema = { userEmail, articleSlug, rate }; - - // check if user is trying to rate his/her own article - const article = await db.Article.findOne({ - where: { slug: articleSlug } - }); - const user = await db.user.findOne({ - where: { id: article.authorId } + let response; + // check if rate is arleady there + const isRate = await db.Rate.findOne({ + where: { articleSlug, userEmail } }); - if (user.email === userEmail) { - return res.status(400).send({ - status: 400, - message: 'You cannot rate your own article' - }); + // update rate rating + if (isRate) { + response = await RateService.update(rateSchema); + } else { + // create rating + response = await RateService.create(rateSchema); } - - // create rating - const createdRate = await RateService.create(rateSchema); - return res.status(200).send({ - status: 200, - message: 'Thank you for rating this article', - data: createdRate - }); + util.setSuccess(200, 'Successfully rated', response); + return util.send(res); } catch (error) { - return res.status(404).send({ - status: 404, - message: error.message - }); + util.setError(500, 'server error contact admin'); + return util.send(res); } } /** - * - * - * @static - * @param {*} req - * @param {*} res - * @returns {Object} return rating information to user - * @memberof UserController - */ - static async updateArticleRating(req, res) { + * + * + * @static + * @param {*} req + * @param {*} res + * @returns {Object} return rating information to user + * @memberof rateController + */ + static async getAllRating(req, res) { try { - // Initialize rating data - const userEmail = req.auth.email; - const { rate } = req.body; - const { articleSlug } = req.params; - - // update article rate - const updatedRate = await RateService.update(articleSlug, userEmail, rate); - if (!updatedRate) { - return res.status(400).send({ - status: 400, - error: `Rating for article with Slug: ${articleSlug} not found` - }); + const count = await db.Rate.count(); + if (count === 0) { + util.setError(404, 'no rate yet made'); + return util.send(res); } - return res.status(200).send({ - status: 200, - message: 'Thank you for rating this article', - newRating: updatedRate - }); + if (req.offset >= count) { + req.offset = 0; + } + const { offset, limit } = req; + // find particular rating + const allRates = await RateService.getAll(offset, limit); + util.setSuccess(200, 'all rates retrieved successfully', allRates); + return util.send(res); } catch (error) { return res.status(404).send({ status: 404, @@ -101,27 +91,39 @@ class rateController { static async getArticleRating(req, res) { try { // Initialize rating data - const userEmail = req.auth.email; const { articleSlug } = req.params; - - // find particular rating - const userRate = await RateService.findOne(articleSlug, userEmail); - if (!userRate) { - return res.status(400).send({ - status: 400, - error: `Rating for article with slug ${articleSlug} not found`, - }); + // check if rate is arleady there + const isArticle = await db.Article.findOne({ + where: { slug: articleSlug } + }); + if (!isArticle) { + util.setError(404, 'post not found'); + return util.send(res); } - return res.status(200).send({ - status: 200, - message: `Rating for article with slug ${articleSlug} found`, - data: userRate + const count = await db.Rate.count({ + where: { articleSlug } }); + if (count === 0) { + util.setError(404, `Article with slug ${articleSlug} not yet Rated`); + return util.send(res); + } + const rating = await RateService.getArticleRatingStatistic(articleSlug); + if (req.offset >= count) { + req.offset = 0; + } + const { offset, limit } = req; + // find particular rating + const ArticleRAting = await RateService.findArticlesRatings(articleSlug, limit, offset); + const response = { + rating, + count, + data: ArticleRAting + }; + util.setSuccess(200, `Rating for article with slug ${articleSlug} found`, response); + return util.send(res); } catch (error) { - return res.status(404).send({ - status: 404, - message: error.message - }); + util.setError(500, 'server error contact admin'); + return util.send(res); } } } diff --git a/src/controllers/stats.controller.js b/src/controllers/stats.controller.js new file mode 100644 index 0000000..436df3b --- /dev/null +++ b/src/controllers/stats.controller.js @@ -0,0 +1,15 @@ +/* eslint-disable require-jsdoc */ +import StatsService from '../services/db.service'; +import { success } from '../helpers/responses'; + +const { getStat } = StatsService; + +class statsController { + static async getStats(req, res) { + const readerId = req.auth.id; + const stats = await getStat({ readerId }, 'Stats'); + return success('your reading stats', stats).send(res); + } +} + +export default statsController; diff --git a/src/helpers/helper.js b/src/helpers/helper.js index 73c5b2c..94d219e 100644 --- a/src/helpers/helper.js +++ b/src/helpers/helper.js @@ -88,5 +88,35 @@ class Helper { const formatedReadTime = `${readTime} min read`; return formatedReadTime; } + + /** + * + * + * @static + * @param {*} likeInfo + * @returns {string} formarted comment like information + * @memberof Helper + */ + static formatLikeInfo(likeInfo) { + let formattedOutput = 'You'; + const usernames = likeInfo.split(', '); + + if (usernames.length === 2) { + formattedOutput += ' like this comment'; + return formattedOutput; + } + for (let i = 1; i < (usernames.length - 1); i += 1) { + if (i === 5) break; + formattedOutput += `, ${usernames[i]} `; + } + + if (usernames.length < 6) { + formattedOutput += ' like this comment'; + return formattedOutput; + } + + formattedOutput += `and ${(usernames.length - 6)} more people like this comment`; + return formattedOutput; + } } export default Helper; diff --git a/src/helpers/responses.js b/src/helpers/responses.js new file mode 100644 index 0000000..f01a817 --- /dev/null +++ b/src/helpers/responses.js @@ -0,0 +1,18 @@ +import Util from './util'; + +const util = new Util(); + +export const notFound = (msg) => { + util.setError(404, `${msg} not found`); + return util; +}; + +export const error = (msg, code = 400) => { + util.setError(code, msg); + return util; +}; + +export const success = (msg, data = null) => { + util.setSuccess(200, msg, data); + return util; +}; diff --git a/src/middlewares/rate.middleware.js b/src/middlewares/rate.middleware.js new file mode 100644 index 0000000..ccdfd6d --- /dev/null +++ b/src/middlewares/rate.middleware.js @@ -0,0 +1,25 @@ +import Util from '../helpers/util'; +import models from '../models/index'; + +const util = new Util(); + +export default async (req, res, next) => { + try { + const ArticleSlug = req.params.articleSlug; + const post = await models.Article.findOne({ where: { slug: ArticleSlug } }); + if (!post) { + util.setError(404, 'post not found'); + return util.send(res); + } + const user = await models.user.findOne({ where: { email: req.auth.email } }); + const userId = user.id; + if (userId === post.authorId) { + util.setError(400, 'You cannot rate your own article'); + return util.send(res); + } + next(); + } catch (error) { + util.setError(500, 'server error contact admin'); + return util.send(res); + } +}; diff --git a/src/middlewares/stats.js b/src/middlewares/stats.js new file mode 100644 index 0000000..656f5e5 --- /dev/null +++ b/src/middlewares/stats.js @@ -0,0 +1,37 @@ +/* eslint-disable require-jsdoc */ + +import jwt from 'jsonwebtoken'; + +import StatsService from '../services/db.service'; +import { notFound, error } from '../helpers/responses'; + +const { getStat } = StatsService; + +class statsWare { + static async checkStats(req, res, next) { + if (!req.auth) { + return error('you are not logged in').send(res); + } + const readerId = req.auth.id; + const stats = await getStat({ readerId }, 'Stats'); + if (!stats.length) { + return notFound('reading stats').send(res); + } + next(); + } + + static async saveStat(req, res, next) { + try { + let token = req.headers['x-access-token'] || req.headers.authorization; + token = token.slice(7, token.length); + jwt.verify(token, process.env.SECRET_KEY, (err, decode) => { + req.auth = decode; + next(); + }); + } catch (err) { + next(); + } + } +} + +export default statsWare; diff --git a/src/middlewares/validators/rate.validation.js b/src/middlewares/validators/rate.validation.js index 9bea05f..07caaea 100644 --- a/src/middlewares/validators/rate.validation.js +++ b/src/middlewares/validators/rate.validation.js @@ -7,7 +7,8 @@ export default (req, res, next) => { const { rate } = req.body; const schema = { - rate: Joi.number().integer().min(1).max(5), + rate: Joi.number().integer().min(1).max(5) + .required(), }; const { error } = Joi.validate({ rate diff --git a/src/migrations/20190805142252-create-user.js b/src/migrations/20190805142252-create-user.js index 3780f98..0508215 100644 --- a/src/migrations/20190805142252-create-user.js +++ b/src/migrations/20190805142252-create-user.js @@ -38,6 +38,14 @@ module.exports = { allowNull: true, type: Sequelize.STRING, }, + bio: { + allowNull: true, + type: Sequelize.STRING, + }, + image: { + allowNull: true, + type: Sequelize.STRING, + }, role: { allowNull: false, type: Sequelize.STRING, diff --git a/src/migrations/20190813125249-create-article.js b/src/migrations/20190813125249-create-article.js index 936dd9d..3e1eecb 100644 --- a/src/migrations/20190813125249-create-article.js +++ b/src/migrations/20190813125249-create-article.js @@ -10,7 +10,7 @@ module.exports = { slug: { type: Sequelize.STRING, allowNull: false, - unique:true, + unique: true, }, title: { type: Sequelize.STRING, @@ -39,12 +39,12 @@ module.exports = { }, authorId: { type: Sequelize.INTEGER, - allowNull:false, + allowNull: false, onUpdate: 'CASCADE', onDelete: 'CASCADE', references: { - model: 'users', - key: 'id' + model: 'users', + key: 'id' } }, images: { diff --git a/src/migrations/20190814093719-create-rate.js b/src/migrations/20190814093719-create-rate.js index 57506f2..b70dafc 100644 --- a/src/migrations/20190814093719-create-rate.js +++ b/src/migrations/20190814093719-create-rate.js @@ -27,9 +27,8 @@ module.exports = { onUpdate: 'CASCADE' }, rate: { - type: Sequelize.ENUM({ - values: [1, 2, 3, 4, 5] - }) + allowNull: false, + type: Sequelize.INTEGER }, createdAt: { allowNull: false, diff --git a/src/migrations/20190816101310-create-comment.js b/src/migrations/20190816101310-create-comment.js index 45596c5..1e556e2 100644 --- a/src/migrations/20190816101310-create-comment.js +++ b/src/migrations/20190816101310-create-comment.js @@ -23,10 +23,18 @@ module.exports = { type: Sequelize.STRING, allowNull: true }, - parentCommentId: { - type: Sequelize.INTEGER, - allowNull: true - }, + parentCommentId: { + type: Sequelize.INTEGER, + allowNull: true + }, + likesCount: { + type: Sequelize.INTEGER, + defaultValue: 0 + }, + likeInfo: { + type: Sequelize.TEXT, + defaultValue: '' + }, createdAt: { allowNull: false, type: Sequelize.DATE @@ -35,14 +43,14 @@ module.exports = { allowNull: false, type: Sequelize.DATE } - }) .then(() => sequelize.query(`ALTER TABLE "Comments" + }).then(() => sequelize.query(`ALTER TABLE "Comments" ADD CONSTRAINT fk_parentReference FOREIGN KEY ("parentCommentId") - REFERENCES "Comments" (id) ON DELETE CASCADE`),(`ALTER TABLE "Comments" + REFERENCES "Comments" (id) ON DELETE CASCADE`), (`ALTER TABLE "Comments" ADD CONSTRAINT fk_parentReference FOREIGN KEY ("commentRevisions") REFERENCES "Comments" (id) ON DELETE CASCADE`)); - + }, down: (queryInterface, Sequelize) => { return queryInterface.dropTable('Comments'); } -}; \ No newline at end of file +}; diff --git a/src/migrations/20190823185735-create-reporting.js b/src/migrations/20190823185735-create-reporting.js index 4f88a56..d43cfe7 100644 --- a/src/migrations/20190823185735-create-reporting.js +++ b/src/migrations/20190823185735-create-reporting.js @@ -10,6 +10,7 @@ module.exports = { }, userId: { type: Sequelize.INTEGER, + allowNull: false, onDelete: 'CASCADE', onUpdate: 'CASCADE', references: { @@ -28,6 +29,7 @@ module.exports = { } }, reason: { + allowNull: false, type: Sequelize.ENUM, values: ['Rules Violation', 'Spam', 'Harassment'], }, diff --git a/src/migrations/20190828152317-create-stats.js b/src/migrations/20190828152317-create-stats.js new file mode 100644 index 0000000..b0ded8b --- /dev/null +++ b/src/migrations/20190828152317-create-stats.js @@ -0,0 +1,33 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('Stats', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + slug: { + type: Sequelize.STRING + }, + item: { + type: Sequelize.STRING + }, + readerId: { + type: Sequelize.INTEGER + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('Stats'); + } +}; \ No newline at end of file diff --git a/src/models/comment.js b/src/models/comment.js index f9d352d..8ef5243 100644 --- a/src/models/comment.js +++ b/src/models/comment.js @@ -12,7 +12,7 @@ module.exports = (sequelize, DataTypes) => { onUpdate: 'CASCADE', onDelete: 'CASCADE' }, - commentRevisions:{ + commentRevisions: { type: DataTypes.STRING, allowNull: true, references: { @@ -28,12 +28,14 @@ module.exports = (sequelize, DataTypes) => { allowNull: true }, body: DataTypes.STRING, + likesCount: DataTypes.INTEGER, + likeInfo: DataTypes.TEXT }, {}); - Comment.associate = function(models) { + Comment.associate = function (models) { // associations can be defined here - Comment.belongsTo(models.user,{ - foreignKey:'id',as:'commentAuthor' + Comment.belongsTo(models.user, { + foreignKey: 'id', as: 'commentAuthor' }); }; return Comment; -}; \ No newline at end of file +}; diff --git a/src/models/follow.js b/src/models/follow.js index 882d06d..242f7a6 100644 --- a/src/models/follow.js +++ b/src/models/follow.js @@ -1,21 +1,21 @@ 'use strict'; module.exports = (sequelize, DataTypes) => { - const Follow = sequelize.define('Follow', { - followerId: DataTypes.INTEGER, - followedUserId: DataTypes.INTEGER - }, {}); - Follow.associate = ({ user }) => { - // associations can be defined here - Follow.belongsTo(user, { - foreignKey: 'followerId', - onDelete: 'CASCADE', - as: 'followerDetails' - }); - Follow.belongsTo(user, { - foreignKey: 'followedUserId', - onDelete: 'CASCADE', - as: 'authorDetails' - }); - }; - return Follow; -}; \ No newline at end of file + const Follow = sequelize.define('Follow', { + followerId: DataTypes.INTEGER, + followedUserId: DataTypes.INTEGER + }, {}); + Follow.associate = ({ user }) => { + // associations can be defined here + Follow.belongsTo(user, { + foreignKey: 'followerId', + onDelete: 'CASCADE', + as: 'followerDetails' + }); + Follow.belongsTo(user, { + foreignKey: 'followedUserId', + onDelete: 'CASCADE', + as: 'authorDetails' + }); + }; + return Follow; +}; diff --git a/src/models/rate.js b/src/models/rate.js index e347d29..ebd0107 100644 --- a/src/models/rate.js +++ b/src/models/rate.js @@ -19,9 +19,7 @@ module.exports = (sequelize, DataTypes) => { onDelete: 'CASCADE', onUpdate: 'CASCADE' }, - rate: DataTypes.ENUM({ - values: [1, 2, 3, 4, 5] - }) + rate: DataTypes.INTEGER, }, {}); Rate.associate = function (models) { Rate.belongsTo(models.user, { diff --git a/src/models/stats.js b/src/models/stats.js new file mode 100644 index 0000000..ed85faf --- /dev/null +++ b/src/models/stats.js @@ -0,0 +1,12 @@ +'use strict'; +module.exports = (sequelize, DataTypes) => { + const Stats = sequelize.define('Stats', { + slug: DataTypes.STRING, + item: DataTypes.STRING, + readerId: DataTypes.INTEGER + }, {}); + Stats.associate = function (models) { + // associations can be defined here + } + return Stats; +}; \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js index e447ef6..5640f91 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -11,14 +11,13 @@ module.exports = (sequelize, DataTypes) => { role: { type: DataTypes.STRING, defaultValue: 'normal' }, verified: { type: DataTypes.BOOLEAN, defaultValue: false }, role: { type: DataTypes.STRING, defaultValue: 'normal' }, - subscribed: {defaultValue: false, type: DataTypes.BOOLEAN }, + subscribed: { defaultValue: false, type: DataTypes.BOOLEAN }, inAppNotification: { defaultValue: false, type: DataTypes.BOOLEAN }, verified: { type: DataTypes.BOOLEAN, defaultValue: false }, }, {}); user.associate = ({ - Follow, Article, Highlight - + Follow, Article, Highlight, Stats }) => { user.hasMany(Follow, { foreignKey: 'followerId', @@ -34,6 +33,10 @@ module.exports = (sequelize, DataTypes) => { targetKey: 'id', onUpdate: 'CASCADE', onDelete: 'CASCADE' + }); + user.hasMany(Stats, { + as: 'reader', + foreignKey: 'readerId' }) user.belongsToMany(Article, { through: 'BookMarks', diff --git a/src/routes/api/article/article.routes.js b/src/routes/api/article/article.routes.js index a3c7fbc..6aa662d 100644 --- a/src/routes/api/article/article.routes.js +++ b/src/routes/api/article/article.routes.js @@ -12,6 +12,7 @@ import tagController from '../../../controllers/tag'; import TagWare from '../../../middlewares/tag.middleware'; import highlight from '../../../controllers/highlight.controller'; import share from '../../../middlewares/shareHighlight.middleware'; +import StatsWare from '../../../middlewares/stats'; const router = express.Router(); const { @@ -24,12 +25,26 @@ const { router.post('/:articleId/favorite', [auth, confirmEmailAuth, validateId], FavoritesController.createOrRemoveFavorite); router.post('/', [auth, confirmEmailAuth], imageUpload.array('images', 10), validate(schema.articleSchema), articleController.createArticles); router.get('/', checkQuery, articleController.getAllArticles); -router.get('/:slug', articleController.getOneArticle); +router.get('/:slug', StatsWare.saveStat, articleController.getOneArticle); router.delete('/:slug', [auth, confirmEmailAuth], articleController.deleteArticle); router.patch('/:slug', [auth, confirmEmailAuth], imageUpload.array('images', 10), articleController.UpdateArticle); router.post('/:slug/share/:channel', [auth, confirmEmailAuth], articleController.shareArticle); +// Highlight +router.post('/:slug/highlight', [auth], highlight.bodyHighlightedText); +router.delete('/highlight/:id', auth, highlight.deleteHighlightComment); +router.get('/:articleId/highlight', auth, highlight.getHighlights); +router.get('/:id/highlight/share/:channel', [auth, share], highlight.shareHightlight); + +// tags + +router.post('/:articleId/tags', [auth, confirmEmailAuth], checkArticle, tagLimit, tagLength, createArticleTag); +router.get('/:articleId/tags', checkArticle, getArticleTags); +router.patch('/:articleId/:name', [auth, confirmEmailAuth], checkArticle, checkTagName, editArticleTag); +router.delete('/:articleId/:name', [auth, confirmEmailAuth], checkArticle, checkTagName, deleteArticleTag); + + // Highlight router.post('/:slug/highlight', [auth], highlight.bodyHighlightedText); router.delete('/highlight/:id', auth, highlight.deleteHighlightComment); diff --git a/src/routes/api/comment/comments.route.js b/src/routes/api/comment/comments.route.js index fef422d..56617c8 100644 --- a/src/routes/api/comment/comments.route.js +++ b/src/routes/api/comment/comments.route.js @@ -2,10 +2,15 @@ import express from 'express'; import Comments from '../../../controllers/comments.controller'; import auth from '../../../middlewares/auth'; import CommentsValidation from '../../../middlewares/validators/comments.body'; +import confirmEmailAuth from '../../../middlewares/emailVarification.middleware'; const router = express.Router(); router.post('/:slug', [auth, CommentsValidation], Comments.createComment); router.get('/', [auth], Comments.getComments); router.delete('/:id', [auth], Comments.deleteComment); router.put('/:id', [auth, CommentsValidation], Comments.updateComment); +router.get('/like/:id', [auth, confirmEmailAuth], Comments.getLikesComments); +router.post('/like/:id', [auth, confirmEmailAuth], Comments.likeComment); +router.put('/like/:id', [auth, confirmEmailAuth], Comments.updateLikeComment); + export default router; diff --git a/src/routes/api/comment/doc.yml b/src/routes/api/comment/doc.yml index 65a23ca..fbe5cfc 100644 --- a/src/routes/api/comment/doc.yml +++ b/src/routes/api/comment/doc.yml @@ -117,6 +117,79 @@ $ref: '#responses/BadRequest' 404: $ref: '#/responses/Notfound' +/comments/like/{id}: + get: + summary: get all likes of a comment + description: > + gets a specific comment like + tags: + - Comment + produces: + - application/json + parameters: + - name: x-access-token + in: header + schema: + type: string + required: + - authorization + responses: + 200: + description: Successfully retrieved comment likes + schema: + $ref: '#definitions/Comment' + 400: + $ref: '#responses/BadRequest' + 404: + $ref: '#/responses/Notfound' + put: + summary: unlike a specific comment + description: > + unlike a specific comment + tags: + - Comment + produces: + - application/json + parameters: + - name: x-access-token + in: header + schema: + type: string + required: + - authorization + responses: + 200: + description: unlike a specific comment + schema: + $ref: '#definitions/Comment' + 400: + $ref: '#responses/BadRequest' + 404: + $ref: '#/responses/Notfound' + post: + summary: like a specific comment + description: > + like a specific comment + tags: + - Comment + produces: + - application/json + parameters: + - name: x-access-token + in: header + schema: + type: string + required: + - authorization + responses: + 200: + description: like a specific comment + schema: + $ref: '#definitions/Comment' + 400: + $ref: '#responses/BadRequest' + 404: + $ref: '#/responses/Notfound' tags: - name: Comment description: Operations related to Comment diff --git a/src/routes/api/rate/rate.route.js b/src/routes/api/rate/rate.route.js index 581d3cd..1e1e5ae 100644 --- a/src/routes/api/rate/rate.route.js +++ b/src/routes/api/rate/rate.route.js @@ -3,11 +3,13 @@ import validateToken from '../../../middlewares/auth'; import confirmEmaiAuth from '../../../middlewares/emailVarification.middleware'; import rateController from '../../../controllers/rating.controller'; import sanitizeRate from '../../../middlewares/validators/rate.validation'; +import { checkQuery } from '../../../middlewares/query.check'; +import rateMiddleware from '../../../middlewares/rate.middleware'; +import admin from '../../../middlewares/admin'; const router = express.Router(); - -router.get('/:articleSlug', [validateToken, confirmEmaiAuth], rateController.getArticleRating); -router.post('/:articleSlug', [validateToken, sanitizeRate, confirmEmaiAuth], rateController.setArticleRating); -router.patch('/:articleSlug', [validateToken, sanitizeRate, confirmEmaiAuth], rateController.updateArticleRating); +router.get('/', [validateToken, confirmEmaiAuth, checkQuery, admin], rateController.getAllRating); +router.get('/:articleSlug', checkQuery, [validateToken, confirmEmaiAuth], rateController.getArticleRating); +router.put('/:articleSlug', [validateToken, sanitizeRate, confirmEmaiAuth, rateMiddleware], rateController.createOrUpdateRate); export default router; diff --git a/src/routes/api/reports/reports.routes.js b/src/routes/api/reports/reports.routes.js index 235a8a5..a6efebd 100644 --- a/src/routes/api/reports/reports.routes.js +++ b/src/routes/api/reports/reports.routes.js @@ -14,9 +14,9 @@ const router = express.Router(); // routes for reporting artiles router.post('/:Article', [auth, confirmEmailAuth, reportValidator, reportMiddleware], reportController.reportArticle); router.delete('/:reportId', [auth, confirmEmailAuth], reportController.deleteReport); -router.get('/', [auth, confirmEmailAuth], reportController.getMyReport); +router.get('/', [auth, confirmEmailAuth, checkQuery], reportController.getMyReport); router.get('/all', [auth, confirmEmailAuth, admin, checkQuery], reportController.getAllReport); -router.get('/:Article', [auth, confirmEmailAuth, admin], reportController.getReportsForArticle); +router.get('/:Article', [auth, confirmEmailAuth, admin, checkQuery], reportController.getReportsForArticle); export default router; diff --git a/src/routes/api/user/user.route.js b/src/routes/api/user/user.route.js index 4ecb698..7133dd7 100644 --- a/src/routes/api/user/user.route.js +++ b/src/routes/api/user/user.route.js @@ -10,6 +10,8 @@ import followController from '../../../controllers/follow.controller'; import resetPasswordValidation from '../../../middlewares/validators/resetpassword.validation'; import BookMarkController from '../../../controllers/bookmarks.controller'; import BookMarkWare from '../../../middlewares/bookmarks'; +import statsController from '../../../controllers/stats.controller'; +import statsWare from '../../../middlewares/stats'; const { checkBookmark, checkUserBookMarks, checkDuplicate, createCopy, @@ -23,6 +25,9 @@ const { unCollect } = BookMarkController; +const { getStats } = statsController; +const { saveStat, checkStats } = statsWare; + // bookmarks routes router.post('/bookmarks/copy', createCopy, copyBookmark); router.patch('/bookmarks/update', createCopy, editBookMark); @@ -40,6 +45,9 @@ router.delete('/bookmarks/:name', [validateToken, confirmEmaiAuth], checkBookmar router.delete('/bookmarks', [validateToken, confirmEmaiAuth], checkUserBookMarks, deleteUserBookMarks); +// stats route +router.get('/stats', saveStat, checkStats, getStats); + router.get('/verify', verifyEmail); router.get('/allusers', [validateToken, admin, confirmEmaiAuth], UserController.getAllUsers); router.post('/signup', validateUser, UserController.signup); @@ -54,6 +62,10 @@ router.get('/profiles/following', validateToken, followController.listOfFollowed router.get('/profiles/followers', validateToken, followController.listOfFollowers); +// reset password route handlers +router.post('/reset', UserController.requestPasswordReset); +router.patch('/reset/:token', resetPasswordValidation, UserController.handlePasswordReset); + // reset password route handlers router.post('/reset', UserController.requestPasswordReset); router.patch('/reset/:token', resetPasswordValidation, UserController.handlePasswordReset); diff --git a/src/seeders/20190808074800-normal-user.js b/src/seeders/20190808074800-normal-user.js index 1130044..ce9fc5a 100644 --- a/src/seeders/20190808074800-normal-user.js +++ b/src/seeders/20190808074800-normal-user.js @@ -51,6 +51,39 @@ export default { createdAt: new Date(), updatedAt: new Date() }, + { + firstname: 'userfive', + lastname: 'userfive', + email: 'userfive@gmail.com', + password: hashPassword, + username: 'userfive', + role: 'normal', + verified: true, + createdAt: new Date(), + updatedAt: new Date() + }, + { + firstname: 'usersix', + lastname: 'usersix', + email: 'usersix@gmail.com', + password: hashPassword, + username: 'usersix', + role: 'normal', + verified: true, + createdAt: new Date(), + updatedAt: new Date() + }, + { + firstname: 'userseven', + lastname: 'userseven', + email: 'userseven@gmail.com', + password: hashPassword, + username: 'userseven', + role: 'normal', + verified: true, + createdAt: new Date(), + updatedAt: new Date() + }, ]), down: queryInterface => queryInterface.bulkDelete('users', null, {}) diff --git a/src/seeders/20190829055129-demo-comment.js b/src/seeders/20190829055129-demo-comment.js new file mode 100644 index 0000000..d62b206 --- /dev/null +++ b/src/seeders/20190829055129-demo-comment.js @@ -0,0 +1,37 @@ +export default { + up: queryInterface => queryInterface.bulkInsert( + 'Comments', + [ + + { + userId: 1, + articleSlug: 'fakeslug', + body: 'comment one', + likesCount: 7, + likeInfo: 'admin, username, usertwo, userthree, userfour, userfive, usersix, userseven, ', + createdAt: new Date(), + updatedAt: new Date() + }, + { + userId: 1, + articleSlug: 'fakeslug', + body: 'comment two', + likesCount: 1, + likeInfo: 'admin, ', + createdAt: new Date(), + updatedAt: new Date() + }, + { + userId: 1, + articleSlug: 'fakeslug', + body: 'comment three', + likesCount: 0, + likeInfo: '', + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ), + down: queryInterface => queryInterface.bulkDelete('Comments', null, {}) +}; diff --git a/src/services/comments.service.js b/src/services/comments.service.js index 86f3ad5..767fd91 100644 --- a/src/services/comments.service.js +++ b/src/services/comments.service.js @@ -63,5 +63,24 @@ class commentsService { throw error; } } + + /** + * + * + * @static + * @param {*} id + * @param {*} updateComments + * @returns {object} data + * @memberof commentsService + */ + static async findOne(id) { + try { + return await database.findOne({ + where: { id } + }); + } catch (error) { + throw error; + } + } } export default commentsService; diff --git a/src/services/db.service.js b/src/services/db.service.js new file mode 100644 index 0000000..84774fd --- /dev/null +++ b/src/services/db.service.js @@ -0,0 +1,18 @@ +/* eslint-disable require-jsdoc */ +import models from '../models'; + +const { Stats, Article } = models; +const Models = { Stats, Article }; +const conditon = where => ({ where }); + +class StatsService { + static async createStat(where, model) { + return Models[model].create(where); + } + + static async getStat(where, model) { + return Models[model].findAll(conditon(where)); + } +} + +export default StatsService; diff --git a/src/services/rate.service.js b/src/services/rate.service.js index 3165754..efcb047 100644 --- a/src/services/rate.service.js +++ b/src/services/rate.service.js @@ -1,5 +1,7 @@ -import database from '../models/index'; +import Sequelize from 'sequelize'; +import models from '../models'; +const db = models.Rate; /** * * @@ -7,19 +9,21 @@ import database from '../models/index'; */ class RateService { /** - * - * - * @static - * @param {*} articleSlug - * @param {*} userEmail - * @returns - * @memberof RateService - * @returns {Object} return db result object - */ - static async findOne(articleSlug, userEmail) { + * + * + * @static + * @param {*} offset + * @param {*} limit + * @param {*} articleSlug + * @memberof RateService + * @returns {Object} return db result object + + */ + static async getAll(offset, limit) { try { - return await database.Rate.findOne({ - where: { articleSlug, userEmail } + return await db.findAll({ + offset, + limit, }); } catch (error) { throw error; @@ -27,61 +31,86 @@ class RateService { } /** - * - * - * @static - * @returns - * @memberof RateService - * @returns {Object} return db result object - */ - static async getAllRatings() { + * + * + * @static + * @param {*} articleSlug + * @memberof RateService + * @returns {Object} return db result object + + */ + static async getArticleRatingStatistic(articleSlug) { try { - return await database.Rate.findAll(); + return await db.findAll({ + where: { articleSlug }, + returning: true, + plain: true, + attributes: [[Sequelize.fn('AVG', Sequelize.col('rate')), 'rating']], + }); } catch (error) { throw error; } } /** - * - * - * @static - * @param {*} rate - * @returns - * @memberof RateService - * @returns {Object} return db result object - */ + * + * + * @static + * @param {*} rate + * @returns + * @memberof RateService + * @returns {Object} return db result object + */ static async create(rate) { try { - return await database.Rate.create(rate); + return await db.create(rate); } catch (error) { throw error; } } /** - * - * - * @static - * @param {*} articleSlug - * @param {*} userEmail - * @param {*} updateRate - * @returns - * @memberof RateService - * @returns {Object} return db result object - */ - static async update(articleSlug, userEmail, updateRate) { + * + * + * @static + * @param {*} articleSlug + * @param {*} limit + * @param {*} offset + * @returns + * @memberof RateService + * @returns {Object} return db result object + */ + static async findArticlesRatings(articleSlug, limit, offset) { try { - const rateToUpdate = await database.Rate.findOne({ - where: { articleSlug } + return await db.findAll({ + where: { articleSlug }, + offset, + limit, }); + } catch (error) { + throw error; + } + } - if (rateToUpdate) { - await database.user.update(updateRate, { where: { articleSlug, userEmail } }); + /** + * + * + * @static + * @param {*} updateRate + * @returns + * @memberof RateService + * @returns {Object} return db result object + */ + static async update(updateRate) { + const { articleSlug, userEmail, rate } = updateRate; - return updateRate; - } - return null; + try { + return await db.update( + { + rate: Number(rate) + }, + { where: { articleSlug, userEmail }, returning: true, plain: true } + ); } catch (error) { throw error; } diff --git a/test/auth-stats.js b/test/auth-stats.js new file mode 100644 index 0000000..a2e39f6 --- /dev/null +++ b/test/auth-stats.js @@ -0,0 +1,67 @@ +import { chai, expect, server } from './test-setup'; + +let token; +describe('Test user stats', () => { + before((done) => { + chai.request(server) + .post('/api/v1/users/login') + .send({ email: 'admin@gmail.com', password: 'ASqw12345' }) + .end((error, res) => { + token = `Bearer ${res.body.token}`; + done(); + }); + }); + describe('test getting stats', () => { + it('should return correct response if none', (done) => { + chai.request(server) + .get('/api/v1/users/stats') + .set('Authorization', token) + .end((err, res) => { + expect(res.status).to.be.equal(404); + expect(res.body.message).to.include('not found'); + done(); + }); + }); + it('should return correct response if user not logged in', (done) => { + chai.request(server) + .get('/api/v1/users/stats') + .end((err, res) => { + expect(res.status).to.be.equal(400); + expect(res.body.message).to.include('not logged in'); + done(); + }); + }); + }); + describe('test stats gathering', () => { + it('should not populate stats if user unauthenticated', (done) => { + chai.request(server) + .get('/api/v1/articles/fakeslug2') + .end(() => { + chai.request(server) + .get('/api/v1/users/stats') + .set('Authorization', token) + .end((err, res) => { + expect(res.status).to.be.equal(404); + expect(res.body.message).to.include('not found'); + done(); + }); + }); + }); + it('should log every read', (done) => { + chai.request(server) + .get('/api/v1/articles/fakeslug2') + .set('Authorization', token) + .end(() => { + chai.request(server) + .get('/api/v1/users/stats') + .set('Authorization', token) + .end((err, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.deep.property('message', 'your reading stats'); + expect(res.body.data).to.be.a('Array'); + done(); + }); + }); + }); + }); +}); diff --git a/test/like.comment.test.js b/test/like.comment.test.js new file mode 100644 index 0000000..bed769a --- /dev/null +++ b/test/like.comment.test.js @@ -0,0 +1,145 @@ +import { chai, server, expect } from './test-setup'; + +let adminToken; +let userToken; +describe('Like comment', () => { + before('Login User', (done) => { + chai + .request(server) + .post('/api/v1/users/login') + .send({ + email: 'admin@gmail.com', + password: 'ASqw12345' + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + adminToken = `Bearer ${res.body.token}`; + }); + chai + .request(server) + .post('/api/v1/users/login') + .send({ + email: 'user@gmail.com', + password: 'ASqw12345' + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + userToken = `Bearer ${res.body.token}`; + done(); + }); + }); + describe('GET likes of a comment', () => { + it('should be able to get likes of a comment when logged in', (done) => { + chai + .request(server) + .get('/api/v1/comments/like/1') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.deep.property('message'); + done(); + }); + }); + it('should be able to get likes of a comment', (done) => { + chai + .request(server) + .get('/api/v1/comments/like/2') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.deep.property('message'); + done(); + }); + }); + it('should return error if comment does not exist', (done) => { + chai + .request(server) + .get('/api/v1/comments/like/9999') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(404); + expect(res.body).to.have.deep.property('message', 'Comment with id: 9999 does not exist.'); + done(); + }); + }); + }); + describe('POST likes of a comment', () => { + it('should return error if comment does not exist', (done) => { + chai + .request(server) + .post('/api/v1/comments/like/9999') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(404); + expect(res.body).to.have.deep.property('message', 'Comment with id: 9999 does not exist.'); + done(); + }); + }); + it('user should be able to like specific comment', (done) => { + chai + .request(server) + .post('/api/v1/comments/like/3') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(201); + expect(res.body).to.have.deep.property('message'); + done(); + }); + }); + it('user should be not able to like specific comment twice', (done) => { + chai + .request(server) + .post('/api/v1/comments/like/3') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(400); + expect(res.body).to.have.deep.property('message', 'You liked this comment already'); + done(); + }); + }); + }); + describe('PUT likes of a comment', () => { + it('should return error if comment does not exist', (done) => { + chai + .request(server) + .put('/api/v1/comments/like/9999') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(404); + expect(res.body).to.have.deep.property('message', 'Comment with id: 9999 does not exist.'); + done(); + }); + }); + it('user should be able to unlike specific comment', (done) => { + chai + .request(server) + .put('/api/v1/comments/like/3') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.deep.property('message', 'You unliked this comment successfully'); + done(); + }); + }); + it('user should be able to unlike specific comment', (done) => { + chai + .request(server) + .put('/api/v1/comments/like/3') + .set('Authorization', adminToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(400); + expect(res.body).to.have.deep.property('message', 'You did not like this comment before'); + done(); + }); + }); + }); +}); diff --git a/test/rates.test.js b/test/rates.test.js index 4a88ef4..6b7d52e 100644 --- a/test/rates.test.js +++ b/test/rates.test.js @@ -1,118 +1,255 @@ import { chai, server, expect } from './test-setup'; -let adminToken; -let userToken; -describe('Users', () => { - before('Login User', (done) => { +let adminToken, usertwotoken, userToken; + +describe('/POST rate article', () => { + it('should login first user', (done) => { chai .request(server) .post('/api/v1/users/login') .send({ - email: 'admin@gmail.com', + email: 'user@gmail.com', password: 'ASqw12345' }) - .set('Accept', 'Application/JSON') .end((error, res) => { - adminToken = `Bearer ${res.body.token}`; + userToken = `Bearer ${res.body.token}`; + expect(res.status).to.be.equal(200); + expect(res.body).to.have.deep.property('message'); + done(); }); + }); + it('should login second user', (done) => { chai .request(server) .post('/api/v1/users/login') .send({ - email: 'user@gmail.com', + email: 'usertwo@gmail.com', + password: 'ASqw12345' + }) + .end((error, res) => { + usertwotoken = `Bearer ${res.body.token}`; + expect(res.status).to.be.equal(200); + expect(res.body).to.have.deep.property('message'); + done(); + }); + }); + it('should login admin', (done) => { + chai + .request(server) + .post('/api/v1/users/login') + .send({ + email: 'admin@gmail.com', password: 'ASqw12345' }) + .end((error, res) => { + adminToken = `Bearer ${res.body.token}`; + expect(res.status).to.be.equal(200); + expect(res.body).to.have.deep.property('message'); + done(); + }); + }); + it('should return an error when there is no rate provided', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslug') + .set('Authorization', userToken) .set('Accept', 'Application/JSON') .end((error, res) => { - userToken = `Bearer ${res.body.token}`; + expect(res.status).to.be.equal(400); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'rate is required'); + done(); + }); + }); + it('ahould return an erro message when rate is not nbr btn 1-5', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslug') + .set('Authorization', userToken) + .send({ + rate: 10 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(400); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'rate must be less than or equal to 5'); + done(); + }); + }); + it('ahould return an erro message when rate is not nbr btn 1-5', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslug') + .set('Authorization', userToken) + .send({ + rate: 0 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(400); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'rate must be larger than or equal to 1'); + done(); + }); + }); + it('should return error when article is not found', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslu') + .set('Authorization', userToken) + .send({ + rate: 1 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(404); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'post not found'); + done(); + }); + }); + it('should return error when user try to get rate for article which is not there', (done) => { + chai + .request(server) + .get('/api/v1/rate/fakeslu') + .set('Authorization', userToken) + .send({ + rate: 1 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(404); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'post not found'); + done(); + }); + }); + it('should return error message when there is no rate yet made', (done) => { + chai + .request(server) + .get('/api/v1/rate') + .set('Authorization', adminToken) + .send({ + rate: 1 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(404); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'no rate yet made'); + done(); + }); + }); + it('should return error message when there is no rate for a certain article yet', (done) => { + chai + .request(server) + .get('/api/v1/rate/fakeslug') + .set('Authorization', userToken) + .send({ + rate: 1 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(404); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'Article with slug fakeslug not yet Rated'); + done(); + }); + }); + it('should return error when user try to rate his or her article', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslug') + .set('Authorization', adminToken) + .send({ + rate: 4 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(400); + expect(res.body).to.have.keys('message', 'status'); + expect(res.body).to.have.deep.property('message', 'You cannot rate your own article'); + done(); + }); + }); + it('should rate an article', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslug') + .set('Authorization', userToken) + .send({ + rate: 4 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.keys('message', 'status', 'data'); + expect(res.body).to.have.deep.property('message', 'Successfully rated'); + done(); + }); + }); + it('should update rate for an article', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslug') + .set('Authorization', userToken) + .send({ + rate: 3 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.keys('message', 'status', 'data'); + expect(res.body).to.have.deep.property('message', 'Successfully rated'); + done(); + }); + }); + it('user two should rate an article', (done) => { + chai + .request(server) + .put('/api/v1/rate/fakeslug') + .set('Authorization', usertwotoken) + .send({ + rate: 1 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.keys('message', 'status', 'data'); + expect(res.body).to.have.deep.property('message', 'Successfully rated'); + done(); + }); + }); + it('should return rates for article and average rate for that article', (done) => { + chai + .request(server) + .get('/api/v1/rate/fakeslug/?page=2&&limit=5') + .set('Authorization', userToken) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.keys('message', 'status', 'data'); + expect(res.body.data).to.have.keys('rating', 'count', 'data'); + expect(res.body).to.have.deep.property('message', 'Rating for article with slug fakeslug found'); done(); }); }); - describe('/POST rate article', () => { - it('should be able to rate article', (done) => { - chai - .request(server) - .post('/api/v1/rate/fakeslug') - .set('Authorization', userToken) - .send({ - rate: 1 - }) - .set('Accept', 'Application/JSON') - .end((error, res) => { - expect(res.status).to.be.equal(200); - expect(res.body).to.have.deep.property('message', 'Thank you for rating this article'); - done(); - }); - }); - it('should return bad request type of error if a validation error occurs', (done) => { - chai - .request(server) - .post('/api/v1/rate/fakeslug') - .set('Authorization', userToken) - .send({ - rate: 8 - }) - .set('Accept', 'Application/JSON') - .end((error, res) => { - expect(res.status).to.be.equal(400); - done(); - }); - }); - it('should return bad request type of error if a article owner tries to rate his/her own article', (done) => { - chai - .request(server) - .post('/api/v1/rate/fakeslug') - .set('Authorization', adminToken) - .send({ - rate: 4 - }) - .set('Accept', 'Application/JSON') - .end((error, res) => { - expect(res.status).to.be.equal(400); - done(); - }); - }); - }); - describe('/PATCH rate article', () => { - it('should be able to rate article', (done) => { - chai - .request(server) - .patch('/api/v1/rate/fakeslug') - .set('Authorization', userToken) - .send({ - rate: 3 - }) - .set('Accept', 'Application/JSON') - .end((error, res) => { - expect(res.status).to.be.equal(200); - expect(res.body).to.have.deep.property('message', 'Thank you for rating this article'); - done(); - }); - }); - }); - describe('/PATCH rate article', () => { - it('should be able to rate article', (done) => { - chai - .request(server) - .get('/api/v1/rate/fakeslug') - .set('Authorization', userToken) - .end((error, res) => { - expect(res.status).to.be.equal(200); - expect(res.body).to.have.deep.property('message', 'Rating for article with slug fakeslug found'); - done(); - }); - }); - it('should return error if rate not found', (done) => { - chai - .request(server) - .get('/api/v1/rate/fakeslug') - .set('Authorization', userToken) - .end((error, res) => { - expect(res.status).to.be.equal(200); - expect(res.body).to.have.deep.property('message', 'Rating for article with slug fakeslug found'); - done(); - }); - }); + it('admin should return all rates and pagination for rates', (done) => { + chai + .request(server) + .get('/api/v1/rate/?page=2&&limit=5') + .set('Authorization', adminToken) + .send({ + rate: 1 + }) + .set('Accept', 'Application/JSON') + .end((error, res) => { + expect(res.status).to.be.equal(200); + expect(res.body).to.have.keys('message', 'status', 'data'); + expect(res.body).to.have.deep.property('message', 'all rates retrieved successfully'); + done(); + }); }); }); diff --git a/test/reporting.test.js b/test/reporting.test.js index 9cc3065..a8a4fab 100644 --- a/test/reporting.test.js +++ b/test/reporting.test.js @@ -190,7 +190,7 @@ describe('/Report an article', () => { it('user should see all his/her report ', (done) => { chai .request(server) - .get('/api/v1/reports') + .get('/api/v1/reports/?page=2&&limit=5') .set('Authorization', usertoken) .end((error, res) => { expect(res).to.be.an('object'); @@ -222,7 +222,7 @@ describe('/Report an article', () => { it('admin should see all reports ', (done) => { chai .request(server) - .get('/api/v1/reports/all') + .get('/api/v1/reports/all/?page=2&&limit=5') .set('Authorization', admintoken) .end((error, res) => { expect(res).to.be.an('object'); @@ -237,7 +237,7 @@ describe('/Report an article', () => { it('admin should see the report for a pecific Article ', (done) => { chai .request(server) - .get('/api/v1/reports/fakeslug2') + .get('/api/v1/reports/fakeslug2/?page=2&&limit=5') .set('Authorization', admintoken) .end((error, res) => { expect(res).to.be.an('object'); diff --git a/test/share.article.test.js b/test/share.article.test.js index ced6d4d..1c10215 100644 --- a/test/share.article.test.js +++ b/test/share.article.test.js @@ -38,7 +38,7 @@ describe('test for sharing an article', () => { }); it('test for sharing an article via mail', async () => { const req = { - params: { slug: 'fakeslug', channel: 'twitter' }, + params: { slug: 'fakeslug', channel: 'mail' }, auth: { email: 'user@gmail.com' }