From 71dbc808be2f15d4d451a10dc071226febb23359 Mon Sep 17 00:00:00 2001 From: Emile-Nsengimana <30442301+Emile-Nsengimana@users.noreply.github.com> Date: Mon, 22 Jul 2019 10:19:21 +0200 Subject: [PATCH] feat(like-dislike): like or dislike an article (#26) - create an endpoint to like or dislike a specific article [Finishes #166789881] --- src/controllers/articleController.js | 10 +- .../likeDislikeArticleController.js | 152 ++ .../20190716155850-create-like-dislikes.js | 38 + src/db/models/likedislikes.js | 13 + src/db/seeders/20190710155910-Articles.js | 12 +- src/db/seeders/20190716184533-likeDislikes.js | 26 + src/middlewares/articleValidation.js | 16 + src/routes/api/articles.js | 8 +- swagger.json | 2128 +++++++++-------- tests/.codeclimate.yml | 8 - tests/articlesTest.js | 12 + 11 files changed, 1373 insertions(+), 1050 deletions(-) create mode 100644 src/controllers/likeDislikeArticleController.js create mode 100644 src/db/migrations/20190716155850-create-like-dislikes.js create mode 100644 src/db/models/likedislikes.js create mode 100644 src/db/seeders/20190716184533-likeDislikes.js delete mode 100644 tests/.codeclimate.yml diff --git a/src/controllers/articleController.js b/src/controllers/articleController.js index e735b6c..6c261b7 100644 --- a/src/controllers/articleController.js +++ b/src/controllers/articleController.js @@ -7,7 +7,7 @@ const { Users, Articles, Tag } = model; /** * artticle controller */ -class articleManager { +class ArticleManager { /** * * @param {object} req @@ -71,7 +71,7 @@ class articleManager { * * @param {object} req * @param {object} res - * @returns{object} acknowledgement message for delete action result + * @returns {object} acknowledgement message for delete action result */ static async removeArticle(req, res) { try { @@ -94,7 +94,7 @@ class articleManager { * * @param {object} req * @param {object} res - * @returns{object} a single article + * @returns {object} a single article */ static async readArticle(req, res) { try { @@ -123,7 +123,7 @@ class articleManager { * * @param {object} req * @param {object} res - * @returns{object} updated article + * @returns {object} updated article */ static async updateArticle(req, res) { try { @@ -179,4 +179,4 @@ class articleManager { } } } -export default articleManager; +export default ArticleManager; diff --git a/src/controllers/likeDislikeArticleController.js b/src/controllers/likeDislikeArticleController.js new file mode 100644 index 0000000..7364dba --- /dev/null +++ b/src/controllers/likeDislikeArticleController.js @@ -0,0 +1,152 @@ +import model from '../db/models/index'; + +const { likeDislikes } = model; + +/** + * like/dislike controller + */ +class LikeDislike { + /** + * + * @param {string} slug + * @returns {integer} number of likes + */ + static async countLikesDislikes(slug) { + const likes = await likeDislikes.findAll({ where: { slug, like: true } }); + const disLikes = await likeDislikes.findAll({ where: { slug, dislike: true } }); + return ([likes.length, disLikes.length]); + } + + /** + * + * @param {string} slug + * @param {integer} id + * @param {object} liked + * @returns {object} like/dislike info + */ + static async revertLikeArticleAction(slug, id, liked) { + await likeDislikes.update({ like: liked }, { where: { slug, userId: id } }); + const likesDislikes = await LikeDislike.countLikesDislikes(slug); + const articleLikeDislikeInfo = { + liked, + disLiked: false, + likes: likesDislikes[0], + disLikes: likesDislikes[1] + }; + return articleLikeDislikeInfo; + } + + /** + * + * @param {integer} id + * @param {string} slug + * @param {string} likedOrDisliked + * @returns {object} like/dislike info + */ + static async createNewLikeOrDislike(id, slug, likedOrDisliked) { + if (likedOrDisliked === 'like') { + const newArticleLike = { userId: id, slug, like: true }; + const likeArticle = await likeDislikes.create(newArticleLike); + const { like, dislike } = likeArticle.dataValues; + const likesDislikes = await LikeDislike.countLikesDislikes(slug); + const isLiked = { + liked: like, + disliked: dislike, + likes: likesDislikes[0], + dislikes: likesDislikes[1] + }; + return isLiked; + } + const newArticleDislike = { userId: id, slug, dislike: true }; + const dislikeArticle = await likeDislikes.create(newArticleDislike); + const { like, dislike } = dislikeArticle.dataValues; + const likesDislikes = await LikeDislike.countLikesDislikes(slug); + const isDisliked = { + liked: like, + disliked: dislike, + likes: likesDislikes[0], + dislikes: likesDislikes[1] + }; + return isDisliked; + } + + /** + * + * @param {object} req + * @param {object} res + * @returns {object} likes + */ + static async likeArticle(req, res) { + const { slug } = req.params; + const { id } = req.user; + const userReactedOnArticle = await likeDislikes.findOne({ where: { slug, userId: id } }); + if (userReactedOnArticle) { + const { like: liked, dislike: disliked } = userReactedOnArticle.dataValues; + if (disliked) { + await LikeDislike.revertDisLikeArticleAction(slug, id, false); + const likeArticle = await LikeDislike.revertLikeArticleAction(slug, id, true); + return res.status(200).json(likeArticle); + } + if (liked) { + const unLikeArticle = await LikeDislike.revertLikeArticleAction(slug, id, false); + return res.status(200).json(unLikeArticle); + } + const likeArticle = await LikeDislike.revertLikeArticleAction(slug, id, true); + return res.status(200).json(likeArticle); + } + const newArticleLike = await LikeDislike.createNewLikeOrDislike(id, slug, 'like'); + return res.status(200).json(newArticleLike); + } + + + /** + * + * @param {string} slug + * @param {integer} id + * @param {object} disLiked + * @returns {object} like/dislike info + */ + static async revertDisLikeArticleAction(slug, id, disLiked) { + await likeDislikes.update({ dislike: disLiked }, { where: { slug, userId: id } }); + const likesDislikes = await LikeDislike.countLikesDislikes(slug); + const articleLikeDislikeInfo = { + liked: false, + disLiked, + likes: likesDislikes[0], + disLikes: likesDislikes[1] + }; + return articleLikeDislikeInfo; + } + + /** + * + * @param {object} req + * @param {object} res + * @returns {object} likes + */ + static async dislikeArticle(req, res) { + const userReactedOnArticle = await likeDislikes + .findOne({ where: { slug: req.params.slug, userId: req.user.id } }); + if (userReactedOnArticle) { + const { like: liked, dislike: disliked } = userReactedOnArticle.dataValues; + if (liked) { + await LikeDislike.revertLikeArticleAction(req.params.slug, req.user.id, false); + const disLikeArticle = await LikeDislike + .revertDisLikeArticleAction(req.params.slug, req.user.id, true); + return res.status(200).json(disLikeArticle); + } + if (disliked) { + const unDisLikeArticle = await LikeDislike + .revertDisLikeArticleAction(req.params.slug, req.user.id, false); + return res.status(200).json(unDisLikeArticle); + } + const disLikeArticle = await LikeDislike + .revertDisLikeArticleAction(req.params.slug, req.user.id, true); + return res.status(200).json(disLikeArticle); + } + const newArticleDislike = await LikeDislike.createNewLikeOrDislike(req.user.id, req.params.slug, 'dislike'); + return res.status(200).json(newArticleDislike); + } +} + +export default LikeDislike; diff --git a/src/db/migrations/20190716155850-create-like-dislikes.js b/src/db/migrations/20190716155850-create-like-dislikes.js new file mode 100644 index 0000000..069abda --- /dev/null +++ b/src/db/migrations/20190716155850-create-like-dislikes.js @@ -0,0 +1,38 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('likeDislikes', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + userId: { + type: Sequelize.INTEGER + }, + slug: { + type: Sequelize.STRING + }, + like: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + dislike: { + type: Sequelize.BOOLEAN, + defaultValue: false + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('likeDislikes'); + } +}; diff --git a/src/db/models/likedislikes.js b/src/db/models/likedislikes.js new file mode 100644 index 0000000..ff2c3cd --- /dev/null +++ b/src/db/models/likedislikes.js @@ -0,0 +1,13 @@ +'use strict'; +module.exports = (sequelize, DataTypes) => { + const likeDislikes = sequelize.define('likeDislikes', { + userId: DataTypes.INTEGER, + slug: DataTypes.STRING, + like: DataTypes.BOOLEAN, + dislike: DataTypes.BOOLEAN + }, {}); + likeDislikes.associate = function(models) { + // associations can be defined here + }; + return likeDislikes; +}; diff --git a/src/db/seeders/20190710155910-Articles.js b/src/db/seeders/20190710155910-Articles.js index 4cfb091..b4bee2e 100644 --- a/src/db/seeders/20190710155910-Articles.js +++ b/src/db/seeders/20190710155910-Articles.js @@ -12,8 +12,7 @@ module.exports = { slug: 'TIA', createdAt: moment.utc().format(), updatedAt: moment.utc().format() - }, - { + },{ title: 'Delete this', body: 'This is Andela', description: 'From the heart and deep on the soul of young African software engineers', @@ -21,8 +20,15 @@ module.exports = { slug: 'dropTIA', createdAt: moment.utc().format(), updatedAt: moment.utc().format() + },{ + title: 'like dislike', + body: 'This is Andela', + description: 'From the heart and deep on the soul of young African software engineers', + authorId: 1, + slug: 'like-africa', + createdAt: moment.utc().format(), + updatedAt: moment.utc().format() }], {}); }, - down: (queryInterface, Sequelize) => {} }; diff --git a/src/db/seeders/20190716184533-likeDislikes.js b/src/db/seeders/20190716184533-likeDislikes.js new file mode 100644 index 0000000..aabc498 --- /dev/null +++ b/src/db/seeders/20190716184533-likeDislikes.js @@ -0,0 +1,26 @@ +import moment from 'moment'; + +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.bulkInsert('likeDislikes', [{ + userId: 1, + slug: 'TIA', + like: true, + dislike: false, + createdAt: moment.utc().format(), + updatedAt: moment.utc().format() + }, + { + userId: 1, + slug: 'TIA2', + like: false, + dislike: false, + createdAt: moment.utc().format(), + updatedAt: moment.utc().format() + }], {}); + }, + + down: (queryInterface, Sequelize) => {} +}; diff --git a/src/middlewares/articleValidation.js b/src/middlewares/articleValidation.js index de45598..14dfe19 100644 --- a/src/middlewares/articleValidation.js +++ b/src/middlewares/articleValidation.js @@ -1,4 +1,7 @@ import Joi from '@hapi/joi'; +import model from '../db/models/index'; + +const { Articles } = model; /** * rating article validaton @@ -32,5 +35,18 @@ class ArticleRatingValidation { req.rate = checkRating.value; next(); } + + /** + * @param {object} req + * @param {object} res + * @param {object} next + * @returns {Object} descriptive error message + */ + static async checkSlug(req, res, next) { + const { slug } = req.params; + const findArticle = await Articles.findOne({ where: { slug } }); + if (!findArticle) { return res.status(404).json({ error: 'article not found' }); } + next(); + } } export default ArticleRatingValidation; diff --git a/src/routes/api/articles.js b/src/routes/api/articles.js index 6437761..24cd99c 100644 --- a/src/routes/api/articles.js +++ b/src/routes/api/articles.js @@ -4,9 +4,9 @@ import auth from '../../middlewares/auth'; import checkUser from '../../middlewares/checkUser'; import { multerUploads } from '../../middlewares/multer'; import { cloudinaryConfig } from '../../db/config/cloudinaryConfig'; - +import likeDislikeController from '../../controllers/likeDislikeArticleController'; import articleRatingControllers from '../../controllers/articleRatingControllers'; -import ArticleRatingValidation from '../../middlewares/articleValidation'; +import articleValidation from '../../middlewares/articleValidation'; const router = express.Router(); router.use('*', cloudinaryConfig); @@ -16,7 +16,9 @@ router.delete('/article/:slug', auth.checkAuthentication, checkUser.isArticleOwn router.get('/articles/:slug', articleController.readArticle); router.put('/articles/:slug', auth.checkAuthentication, checkUser.isArticleOwner, multerUploads, articleController.updateArticle); router.get('/articles', articleController.listAllArticles); -router.post('/articles/:slug/ratings', auth.checkAuthentication, ArticleRatingValidation.rating, articleRatingControllers.rateArticle); +router.post('/articles/:slug/ratings', auth.checkAuthentication, articleValidation.rating, articleRatingControllers.rateArticle); router.get('/ratings/:articleId', articleRatingControllers.ratingAverage); +router.post('/articles/like/:slug', auth.checkAuthentication, articleValidation.checkSlug, likeDislikeController.likeArticle); +router.post('/articles/dislike/:slug', auth.checkAuthentication, articleValidation.checkSlug, likeDislikeController.dislikeArticle); export default router; diff --git a/swagger.json b/swagger.json index ef3f03f..9ee2946 100644 --- a/swagger.json +++ b/swagger.json @@ -1,1126 +1,1192 @@ { - "swagger": "2.0", - "info": { - "title": "Authors Haven Documentation API", - "version": "1.0.0", - "description": "Authors Haven Documentation with Swagger" - }, - "host": "ah-lobos-backend-swagger.herokuapp.com", - "basePath": "/", - "tags": [ - { - "name": "users", - "description": "Create new User" - } - ], - "schemes": [ - "https", - "http" - ], - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml" - ], - "definitions": { - "signup": { - "required": [ - "username", - "email", - "password" - ], - "properties": { - "username": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "login": { - "required": [ - "username", - "email", - "password" - ], - "properties": { - "username": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "forgot-password": { - "required": [ - "email" - ], - "properties": { - "email": { - "type": "string" - } - } - }, - "reset-password": { - "required": [ - "password", - "confirmPassword" - ], - "properties": { - "password": { - "type": "string" - }, - "confirmPassword": { - "type": "string" - } - } - }, - "logout": { - "required": [ - "token" - ], - "properties": { - "token": { - "type": "text" - } - } - }, - "view-profile": { - "required": [ - "username", - "token" - ], - "properties": { - "username": { - "type": "string" - }, - "token": { - "type": "string" - } - } - }, - "update-profile": { - "properties": { - "username": { - "type": "string" - }, - "email": { - "type": "string" - }, - "image": { - "type": "string" - }, - "bio": { - "type": "string" - } - } - }, - "create-article": { - "required": [ - "title", - "body", - "description" - ], - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "body": { - "type": "string" - }, - "tagList": { - "type": "string" - } - } - }, - "display-specific-article": { - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" - } - } - }, - "delete-specific-article": { - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" - } - } - }, - "update-article": { - "required": [ - "title", - "description", - "body", - "token" - ], - "properties": { - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "body": { - "type": "string" - } - } - }, - "rating": { - "required": [ - "rating" - ], - "properties": { - "rating": { - "type": "integer" - }, - "comment": { - "type": "string" - } - } - }, - "fecthratings": { - "required": [ - "articleid" - ], - "properties": { - "articleid": { - "type": "integer" - } - } - }, - "create-comment": { - "required": [ - "body", - "slug", - "user" - ], - "properties": { - "body": { - "type": "string" - } - } - }, - "delete-comment": { - "required": [ - "body", - "slug", - "id" - ], - "properties": { - "id": { - "type": "integer" - } - } - }, - "get-comments": { - "required": [ - "slug" - ], - "properties": { - "slug": { - "type": "string" - } - } - }, - "followUser": { + "swagger": "2.0", + "info": { + "title": "Authors Haven Documentation API", + "version": "1.0.0", + "description": "Authors Haven Documentation with Swagger" + }, + "host": "ah-lobos-backend-swagger.herokuapp.com", + "basePath": "/", + "tags": [ + { + "name": "users", + "description": "Create new User" + } + ], + "schemes": [ + "https", + "http" + ], + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "definitions": { + "signup": { + "required": [ + "username", + "email", + "password" + ], + "properties": { + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "login": { + "required": [ + "username", + "email", + "password" + ], + "properties": { + "username": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "forgot-password": { + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, + "reset-password": { + "required": [ + "password", + "confirmPassword" + ], + "properties": { + "password": { + "type": "string" + }, + "confirmPassword": { + "type": "string" + } + } + }, + "logout": { "required": [ - "token", - "username" + "token" ], "properties": { "token": { + "type": "text" + } + } + }, + "view-profile": { + "required": [ + "username", + "token" + ], + "properties": { + "username": { "type": "string" }, + "token": { + "type": "string" + } + } + }, + "update-profile": { + "properties": { "username": { "type": "string" + }, + "email": { + "type": "string" + }, + "image": { + "type": "string" + }, + "bio": { + "type": "string" + } + } + }, + "create-article": { + "required": [ + "title", + "body", + "description" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "body": { + "type": "string" + }, + "tagList": { + "type": "string" + } + } + }, + "display-specific-article": { + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "delete-specific-article": { + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "update-article": { + "required": [ + "title", + "description", + "body", + "token" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "body": { + "type": "string" + } + } + }, + "rating": { + "required": [ + "rating" + ], + "properties": { + "rating": { + "type": "integer" + }, + "comment": { + "type": "string" + } + } + }, + "fecthratings": { + "required": [ + "articleid" + ], + "properties": { + "articleid": { + "type": "integer" + } + } + }, + "create-comment": { + "required": [ + "body", + "slug", + "user" + ], + "properties": { + "body": { + "type": "string" + } + } + }, + "delete-comment": { + "required": [ + "body", + "slug", + "id" + ], + "properties": { + "id": { + "type": "integer" + } + } + }, + "get-comments": { + "required": [ + "slug" + ], + "properties": { + "slug": { + "type": "string" + } + } + }, + "followUser": { + "required": [ + "token", + "username" + ], + "properties": { + "token": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "unfollowUser": { + "required": [ + "token", + "username" + ], + "properties": { + "token": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "viewFollowers": { + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "viewFollowees": { + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + } + }, + "paths": { + "/api/users": { + "post": { + "tags": [ + "users" + ], + "description": "Create new User", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "User create a new account", + "require": true, + "schema": { + "$ref": "#/definitions/signup" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "User Account created successfully", + "schema": { + "$ref": "#/definitions/signup" + } + }, + "409": { + "description": "User Account was not created" + } + } + } + }, + "/api/user/forgot-password": { + "post": { + "tags": [ + "users" + ], + "description": "Forgot password", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "User forgot his/her password", + "require": true, + "schema": { + "$ref": "#/definitions/forgot-password" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Please check your email for reset password link", + "schema": { + "$ref": "#/definitions/forgot-password" + } + }, + "400": { + "description": "Please use the email you used when signing up" + } + } + } + }, + "/api/user/reset-password/{userToken}": { + "post": { + "tags": [ + "users" + ], + "description": "Reset password via email link", + "parameters": [ + { + "name": "userToken", + "in": "path", + "description": "A token found on the link after \"reset-password/\"", + "require": true + }, + { + "name": "body", + "in": "body", + "description": "User can reset their password through the use of a reset link sent to their email", + "require": true, + "schema": { + "$ref": "#/definitions/reset-password" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Password succesfully changed", + "schema": { + "$ref": "#/definitions/reset-password" + } + }, + "400": { + "description": "Password reset failed please try again" + } + } + } + }, + "/api/profile/{username}": { + "get": { + "tags": [ + "profiles" + ], + "description": "A user can view his or her profile and that of other users", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "A user can view his or her profile and that of other users by specifing the username", + "require": true + }, + { + "name": "token", + "in": "header", + "description": "Token to authorized user", + "require": true + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "user profile fetched successfully", + "schema": { + "$ref": "#/definitions/view-profile" + } + } + } + } + }, + "/api/profiles/{username}": { + "put": { + "tags": [ + "profiles" + ], + "description": "A user can update his or her profile and that of other users", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "A user can update his or her profile and that of other users by specifing the username", + "require": true + }, + { + "name": "token", + "in": "header", + "description": "Token to authorized user", + "require": true + }, + { + "name": "body", + "in": "body", + "description": "A user can update his or her profile and that of other users", + "require": true, + "schema": { + "$ref": "#/definitions/update-profile" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "user profile updated successfully", + "schema": { + "$ref": "#/definitions/update-profile" + } + } + } + } + }, + "/api/users/login": { + "post": { + "tags": [ + "users" + ], + "description": "Login a user", + "parameters": [ + { + "name": "body", + "in": "body", + "description": "User login", + "require": true, + "schema": { + "$ref": "#/definitions/login" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "login succesfull", + "schema": { + "$ref": "#/definitions/login" + } + }, + "404": { + "description": "user with email does not exist" + }, + "401": { + "description": "incorrect password" + }, + "500": { + "description": "internal server error! please try again later" + } + } + } + }, + "/api/users/logout": { + "post": { + "tags": [ + "users" + ], + "description": "Log out a user", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "Token", + "required": true + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "User logged out", + "schema": { + "$ref": "#/definitions/logout" + } + } + } + } + }, + "/api/articles/": { + "post": { + "tags": [ + "articles" + ], + "description": "create a new article", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "user token", + "type":"string", + "required":true + }, + { + "name": "body", + "in": "body", + "description": "registered user can create an article", + "schema": { + "$ref": "#/definitions/create-article" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "article posted" + }, + "401": { + "description": "please login or signup" + } + } + } + }, + "/api/articles": { + "get": { + "tags": [ + "articles" + ], + "description": "list all articles", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "articles" + }, + "404": { + "description": "no articles found" + } + } + } + }, + "/api/articles/{slug}": { + "get": { + "tags": [ + "articles" + ], + "description": "display a specific article", + "parameters": [ + { + "name": "slug", + "in": "path", + "type": "string", + "description": "a slug that uniquelly identifies an article", + "require": true + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/display a specific article" + } + }, + "401": { + "description": "you are not allowed to perfom this action" + }, + "404": { + "description": "article not found" + } + } + } + }, + "/api/articles/{slug}/": { + "put": { + "tags": [ + "articles" + ], + "description": "update a specific article", + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "a slug that uniquelly identifies an article", + "require": true + }, + { + "name": "token", + "in": "header", + "description": "a token for authentication and authorization", + "require": true, + "schema": { + "$ref": "#/definitions/update-article" + } + }, + { + "name": "body", + "in": "body", + "description": "owner of an article can update an article", + "require": true, + "schema": { + "$ref": "#/definitions/update-article" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/update-article" + } + }, + "401": { + "description": "you are not allowed to perfom this action" + }, + "404": { + "description": "article not found" + } } } }, - "unfollowUser": { - "required": [ - "token", - "username" - ], - "properties": { - "token": { - "type": "string" - }, - "username": { - "type": "string" + "/api/article/{slug}": { + "delete": { + "tags": [ + "articles" + ], + "description": "delete a specific article", + "parameters": [ + { + "name": "slug", + "in": "path", + "description": "a slug that uniquelly identifies an article", + "require": true + }, + { + "name": "token", + "in": "header", + "description": "a token for authentication and authorization", + "require": true, + "schema": { + "$ref": "#/definitions/delete-specific-article" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "article deleted successful" + }, + "401": { + "description": "you are not allowed to perfom this action" + }, + "404": { + "description": "article not found" + } } } }, - "viewFollowers": { - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" + "/google": { + "get": { + "tags": [ + "users" + ], + "description": "Login with google", + "parameters": [ + { + "name": "google", + "in": "path", + "description": "Login with goolge", + "require": true + } + ], + "responses": { + "200": { + "description": "Google" + } } } }, - "viewFollowees": { - "required": [ - "token" - ], - "properties": { - "token": { - "type": "string" + "/facebook": { + "get": { + "tags": [ + "users" + ], + "description": "Login with Facebook", + "parameters": [ + { + "name": "google", + "in": "path", + "description": "Login with Facebook", + "require": true + } + ], + "responses": { + "200": { + "description": "Facebook" + } } } - } - }, - "paths": { - "/api/users": { - "post": { - "tags": [ - "users" - ], - "description": "Create new User", - "parameters": [ - { - "name": "body", - "in": "body", - "description": "User create a new account", - "require": true, - "schema": { - "$ref": "#/definitions/signup" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "201": { - "description": "User Account created successfully", - "schema": { - "$ref": "#/definitions/signup" - } - }, - "409": { - "description": "User Account was not created" - } - } - } - }, - "/api/user/forgot-password": { - "post": { - "tags": [ - "users" - ], - "description": "Forgot password", - "parameters": [ - { - "name": "body", - "in": "body", - "description": "User forgot his/her password", - "require": true, - "schema": { - "$ref": "#/definitions/forgot-password" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Please check your email for reset password link", - "schema": { - "$ref": "#/definitions/forgot-password" - } - }, - "400": { - "description": "Please use the email you used when signing up" - } - } - } - }, - "/api/user/reset-password/{userToken}": { - "post": { - "tags": [ - "users" - ], - "description": "Reset password via email link", - "parameters": [ - { - "name": "userToken", - "in": "path", - "description": "A token found on the link after \"reset-password/\"", - "require": true - }, - { - "name": "body", - "in": "body", - "description": "User can reset their password through the use of a reset link sent to their email", - "require": true, - "schema": { - "$ref": "#/definitions/reset-password" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Password succesfully changed", - "schema": { - "$ref": "#/definitions/reset-password" - } - }, - "400": { - "description": "Password reset failed please try again" - } - } - } - }, - "/api/profile/{username}": { - "get": { - "tags": [ - "profiles" - ], - "description": "A user can view his or her profile and that of other users", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "A user can view his or her profile and that of other users by specifing the username", - "require": true - }, - { - "name": "token", - "in": "header", - "description": "Token to authorized user", - "require": true - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "user profile fetched successfully", - "schema": { - "$ref": "#/definitions/view-profile" - } - } - } - } - }, - "/api/profiles/{username}": { - "put": { - "tags": [ - "profiles" - ], - "description": "A user can update his or her profile and that of other users", - "parameters": [ - { - "name": "username", - "in": "path", - "description": "A user can update his or her profile and that of other users by specifing the username", - "require": true - }, - { - "name": "token", - "in": "header", - "description": "Token to authorized user", - "require": true - }, - { - "name": "body", - "in": "body", - "description": "A user can update his or her profile and that of other users", - "require": true, - "schema": { - "$ref": "#/definitions/update-profile" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "user profile updated successfully", - "schema": { - "$ref": "#/definitions/update-profile" - } - } - } - } - }, - "/api/users/login": { - "post": { - "tags": [ - "users" - ], - "description": "Login a user", - "parameters": [ - { - "name": "body", - "in": "body", - "description": "User login", - "require": true, - "schema": { - "$ref": "#/definitions/login" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "login succesfull", - "schema": { - "$ref": "#/definitions/login" - } - }, - "404": { - "description": "user with email does not exist" - }, - "401": { - "description": "incorrect password" - }, - "500": { - "description": "internal server error! please try again later" - } - } - } - }, - "/api/users/logout": { - "post": { - "tags": [ - "users" - ], - "description": "Log out a user", - "parameters": [ - { - "name": "token", - "in": "header", - "description": "Token", - "required": true - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "User logged out", - "schema": { - "$ref": "#/definitions/logout" - } - } - } - } - }, - "/api/articles/": { - "post": { - "tags": [ - "articles" - ], - "description": "create a new article", - "parameters": [ - { - "name": "token", - "in": "header", - "description": "user token", - "type":"string", - "required":true - }, - { - "name": "body", - "in": "body", - "description": "registered user can create an article", - "schema": { - "$ref": "#/definitions/create-article" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "201": { - "description": "article posted" - }, - "401": { - "description": "please login or signup" - } - } - } - }, - "/api/articles": { - "get": { - "tags": [ - "articles" - ], - "description": "list all articles", - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "articles" - }, - "404": { - "description": "no articles found" - } - } - } - }, - "/api/articles/{slug}": { - "get": { - "tags": [ - "articles" - ], - "description": "display a specific article", - "parameters": [ - { - "name": "slug", - "in": "path", - "type": "string", - "description": "a slug that uniquelly identifies an article", - "require": true - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/display a specific article" - } - }, - "401": { - "description": "you are not allowed to perfom this action" - }, - "404": { - "description": "article not found" - } - } - } - }, - "/api/articles/{slug}/": { - "put": { - "tags": [ - "articles" - ], - "description": "update a specific article", - "parameters": [ - { - "name": "slug", - "in": "path", - "description": "a slug that uniquelly identifies an article", - "require": true - }, - { - "name": "token", - "in": "header", - "description": "a token for authentication and authorization", - "require": true, - "schema": { - "$ref": "#/definitions/update-article" - } - }, - { - "name": "body", - "in": "body", - "description": "owner of an article can update an article", - "require": true, - "schema": { - "$ref": "#/definitions/update-article" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "schema": { - "$ref": "#/definitions/update-article" - } - }, - "401": { - "description": "you are not allowed to perfom this action" - }, - "404": { - "description": "article not found" - } - } - } - }, - "/api/article/{slug}": { - "delete": { - "tags": [ - "articles" - ], - "description": "delete a specific article", - "parameters": [ - { - "name": "slug", - "in": "path", - "description": "a slug that uniquelly identifies an article", - "require": true - }, - { - "name": "token", - "in": "header", - "description": "a token for authentication and authorization", - "require": true, - "schema": { - "$ref": "#/definitions/delete-specific-article" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "article deleted successful" - }, - "401": { - "description": "you are not allowed to perfom this action" - }, - "404": { - "description": "article not found" - } - } - } - }, - "/google": { - "get": { - "tags": [ - "users" - ], - "description": "Login with google", - "parameters": [ - { - "name": "google", - "in": "path", - "description": "Login with goolge", - "require": true - } - ], - "responses": { - "200": { - "description": "Google" - } - } - } - }, - "/facebook": { - "get": { - "tags": [ - "users" - ], - "description": "Login with Facebook", - "parameters": [ - { - "name": "google", - "in": "path", - "description": "Login with Facebook", - "require": true - } - ], - "responses": { - "200": { - "description": "Facebook" - } - } - } - }, - "/twitter": { - "get": { - "tags": [ - "users" - ], - "description": "Login with Twitter", - "parameters": [ - { - "name": "google", - "in": "path", - "description": "Login with Twitter", - "require": true - } - ], - "responses": { - "200": { - "description": "Twitter" - } - } - } - }, - "/api/articles/{slug}/ratings": { - "post": { - "tags": [ - "articles" - ], - "description": "User should be able to rate an article", - "parameters": [ - { - "name": "token", - "in": "header", - "type": "string", - "required": true, - "description": "User Token" - }, - { - "name": "slug", - "in": "path", - "required": true, - "type": "string", - "description": "Article Slug" - }, - { - "name": "rating", - "in": "body", - "required": true, - "description": "Rating number", - "schema": { - "$ref": "#/definitions/rating" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Article rated successfuly" - }, - "400": { - "description": "Rate must be one of [1, 2, 3, 4, 5]" - }, - "404": { - "description": "Article not found" - }, - "500": { - "description": "internal server error" - } - } - } - }, - "/api/ratings/{articleid}": { - "get": { - "tags": [ - "articles" - ], - "description": "Fetch a rating", - "parameters": [ - { - "name": "articleid", - "in": "path", - "description": "Fetch ratings", - "require": true, - "schema": { - "$ref": "#/definitions/fecthratings" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "Ratings", - "schema": { - "$ref": "#/definitions/fecthratings" - } - } - } - } - }, - "/api/users/profile": { - "get": { - "tags": [ - "profiles" - ], - "description": "Get all authors profiles", - "parameters": [ - { - "name": "token", - "in": "header", - "description": "Get the token after login", - "require": true - } - ], - "responses": { - "200": { - "description": "All authors profiles found" - }, - "500": { - "description": "The server error was found" - } - } - } - }, - "/api/articles/{slug}/comments/": { - "post": { - "tags": [ - "comments" - ], - "description": "Create new Comment", - "parameters": [ - { - "name": "token", - "in": "header", - "type": "string", - "required": true, - "description": "User Token" - }, - { - "name": "slug", - "in": "path", - "required": true, - "type": "string", - "description": "Article Slug" - }, - { - "name": "comment", - "in": "body", - "required": true, - "description": "Comment on article", - "schema": { - "$ref": "#/definitions/create-comment" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "201": { - "description": "Thank you for commenting on our article" - }, - "404": { - "description": "The Article requested is not found!" - }, - "409": { - "description": "Internal Server Error" - } - } - } - }, - "/api/articles/{slug}/comments/{id}": { - "delete": { - "tags": [ - "comments" - ], - "description": "delete a Comment", - "parameters": [ - { - "name": "token", - "in": "header", - "type": "string", - "required": true, - "description": "User Token" - }, - { - "name": "slug", - "in": "path", - "required": true, - "type": "string", - "description": "Article Slug" - }, - { - "name": "id", - "in": "path", - "required": true, - "description": "delete a comment", - "schema": { - "$ref": "#/definitions/delete-comment" - } - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "The comment was deleted successfully" - }, - "404": { - "description": "The comment requested is not found!" - }, - "500": { - "description": "Internal Server Error" - } - } - } - }, - "/api/articles/{slug}/comment/": { - "get": { - "tags": [ - "comments" - ], - "description": "get all Comment", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "type": "string", - "description": "Article Slug" - } - ], - "produces": [ - "application/json" - ], - "responses": { - "200": { - - }, - "404": { - "description": "The Article requested has not been rated yet!" - } - } - } - }, - "/api/profiles/{username}/follow": { + }, + "/twitter": { + "get": { + "tags": [ + "users" + ], + "description": "Login with Twitter", + "parameters": [ + { + "name": "google", + "in": "path", + "description": "Login with Twitter", + "require": true + } + ], + "responses": { + "200": { + "description": "Twitter" + } + } + } + }, + "/api/articles/{slug}/ratings": { "post": { "tags": [ - "followers" + "articles" ], - "description": "A user can follow another user", + "description": "User should be able to rate an article", "parameters": [ { "name": "token", "in": "header", - "description": "Get the token after login", - "require": true + "type": "string", + "required": true, + "description": "User Token" }, { - "name": "username", + "name": "slug", "in": "path", - "description": "The user you want to follow", - "require": true + "required": true, + "type": "string", + "description": "Article Slug" + }, + { + "name": "rating", + "in": "body", + "required": true, + "description": "Rating number", + "schema": { + "$ref": "#/definitions/rating" + } } ], + "produces": [ + "application/json" + ], "responses": { "200": { - "description": "user followed successfully" - }, - "409": { - "description": "you are already following this user" + "description": "Article rated successfuly" }, "400": { - "description": "you can not follow yourself" + "description": "Rate must be one of [1, 2, 3, 4, 5]" }, "404": { - "description": "user does not exist" + "description": "Article not found" + }, + "500": { + "description": "internal server error" } } } }, - "/api/profiles/{username}/unfollow": { - "delete": { + "/api/ratings/{articleid}": { + "get": { + "tags": [ + "articles" + ], + "description": "Fetch a rating", + "parameters": [ + { + "name": "articleid", + "in": "path", + "description": "Fetch ratings", + "require": true, + "schema": { + "$ref": "#/definitions/fecthratings" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Ratings", + "schema": { + "$ref": "#/definitions/fecthratings" + } + } + } + } + }, + "/api/users/profile": { + "get": { "tags": [ - "followers" + "profiles" ], - "description": "A user can unfollow a user they already follow", + "description": "Get all authors profiles", "parameters": [ { "name": "token", "in": "header", "description": "Get the token after login", "require": true + } + ], + "responses": { + "200": { + "description": "All authors profiles found" }, + "500": { + "description": "The server error was found" + } + } + } + }, + "/api/articles/{slug}/comments/": { + "post": { + "tags": [ + "comments" + ], + "description": "Create new Comment", + "parameters": [ { - "name": "username", + "name": "token", + "in": "header", + "type": "string", + "required": true, + "description": "User Token" + }, + { + "name": "slug", "in": "path", - "description": "The user you want to unfollow", - "require": true + "required": true, + "type": "string", + "description": "Article Slug" + }, + { + "name": "comment", + "in": "body", + "required": true, + "description": "Comment on article", + "schema": { + "$ref": "#/definitions/create-comment" + } } ], + "produces": [ + "application/json" + ], "responses": { - "200": { - "description": "user unfollowed successfully" - }, - "400": { - "description": "you can not follow yourself" + "201": { + "description": "Thank you for commenting on our article" }, "404": { - "description": "user does not exist" + "description": "The Article requested is not found!" + }, + "409": { + "description": "Internal Server Error" } } } }, - "/api/profiles/followers": { - "get": { + "/api/articles/{slug}/comments/{id}": { + "delete": { "tags": [ - "followers" + "comments" ], - "description": "A user can view their followers", + "description": "delete a Comment", "parameters": [ { "name": "token", "in": "header", - "description": "Get the token after login", - "require": true + "type": "string", + "required": true, + "description": "User Token" + }, + { + "name": "slug", + "in": "path", + "required": true, + "type": "string", + "description": "Article Slug" + }, + { + "name": "id", + "in": "path", + "required": true, + "description": "delete a comment", + "schema": { + "$ref": "#/definitions/delete-comment" + } } ], + "produces": [ + "application/json" + ], "responses": { "200": { - "description": "your followers have been fetched successfully" + "description": "The comment was deleted successfully" }, "404": { - "description": "You have zero(0) followers" + "description": "The comment requested is not found!" + }, + "500": { + "description": "Internal Server Error" } } } }, - "/api/profiles/followees": { + "/api/articles/{slug}/comment/": { "get": { "tags": [ - "followers" + "comments" ], - "description": "A user can view the users they follow", + "description": "get all Comment", "parameters": [ { - "name": "token", - "in": "header", - "description": "Get the token after login", - "require": true + "name": "slug", + "in": "path", + "required": true, + "type": "string", + "description": "Article Slug" } ], + "produces": [ + "application/json" + ], "responses": { "200": { - "description": "The users you follow have been fetched successfully" + }, "404": { - "description": "You are not following anyone yet" + "description": "The Article requested has not been rated yet!" } } } + }, + "/api/profiles/{username}/follow": { + "post": { + "tags": [ + "followers" + ], + "description": "A user can follow another user", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "Get the token after login", + "require": true + }, + { + "name": "username", + "in": "path", + "description": "The user you want to follow", + "require": true + } + ], + "responses": { + "200": { + "description": "user followed successfully" + }, + "409": { + "description": "you are already following this user" + }, + "400": { + "description": "you can not follow yourself" + }, + "404": { + "description": "user does not exist" + } + } + } + }, + "/api/profiles/{username}/unfollow": { + "delete": { + "tags": [ + "followers" + ], + "description": "A user can unfollow a user they already follow", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "Get the token after login", + "require": true + }, + { + "name": "username", + "in": "path", + "description": "The user you want to unfollow", + "require": true + } + ], + "responses": { + "200": { + "description": "user unfollowed successfully" + }, + "400": { + "description": "you can not follow yourself" + }, + "404": { + "description": "user does not exist" + } + } + } + }, + "/api/profiles/followers": { + "get": { + "tags": [ + "followers" + ], + "description": "A user can view their followers", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "Get the token after login", + "require": true + } + ], + "responses": { + "200": { + "description": "your followers have been fetched successfully" + }, + "404": { + "description": "You have zero(0) followers" + } + } + } + }, + "/api/profiles/followees": { + "get": { + "tags": [ + "followers" + ], + "description": "A user can view the users they follow", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "Get the token after login", + "require": true + } + ], + "responses": { + "200": { + "description": "The users you follow have been fetched successfully" + }, + "404": { + "description": "You are not following anyone yet" + } + } + } + }, + "/api/articles/like/{slug}": { + "post": { + "tags": [ + "articles" + ], + "description": "like an article", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "token for authentication and authorization", + "require": true + }, + { + "name": "slug", + "in": "path", + "description": "slug that identifies the token", + "require": true + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "article liked" + }, + "404": { + "description": "article not found" + } + } + } + }, + "/api/articles/dislike/{slug}": { + "post": { + "tags": [ + "articles" + ], + "description": "dislike an article", + "parameters": [ + { + "name": "token", + "in": "header", + "description": "token for authentication and authorization", + "require": true + }, + { + "name": "slug", + "in": "path", + "description": "slug that identifies the token", + "require": true + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "article disliked" + }, + "400": { + "description": "article not found" + } + } } } + } } diff --git a/tests/.codeclimate.yml b/tests/.codeclimate.yml deleted file mode 100644 index 129cafb..0000000 --- a/tests/.codeclimate.yml +++ /dev/null @@ -1,8 +0,0 @@ -exclude_patterns: -- "src/db/" -- "tests/" -checks: - file-lines: - enabled: false - method-lines: - enabled: false diff --git a/tests/articlesTest.js b/tests/articlesTest.js index 278c5a4..f3da949 100644 --- a/tests/articlesTest.js +++ b/tests/articlesTest.js @@ -201,4 +201,16 @@ describe('Article test', () => { }); done(); }); + it('should like the article', (done) => { + chai.request(index) + .post('/api/articles/like/like-africa') + .set('token', userToken) + .end((err, res) => { + res.body.should.be.an('object'); + res.body.should.have.property('liked'); + res.body.should.have.property('disliked'); + res.body.liked.should.equal(true); + }); + done(); + }); });