From 0fcca803fa97f573f8c0f5012b23ea9e9131340f Mon Sep 17 00:00:00 2001 From: murediane Date: Thu, 27 Jun 2019 15:47:43 +0200 Subject: [PATCH] [feature (create)]create article endpoint [finish 166240826] --- package.json | 25 +-- src/api/controllers/articleController.js | 43 +++-- src/api/controllers/verifyEmail.js | 21 +-- .../migrations/20190620193548-create-token.js | 22 --- .../20190622195901-create-category.js | 4 +- .../20190622195902-create-article.js | 17 +- .../20190624184203-create-article.js | 59 ------- src/api/models/article.js | 20 ++- src/api/models/category.js | 6 +- src/api/models/token.js | 8 - src/api/routes/articleRouter.js | 5 + src/api/routes/articles.js | 13 -- src/api/seeders/20190621184055-user.js | 2 +- src/api/seeders/20190623142910-category.js | 38 +++++ src/api/seeders/20190624184857-Article.js | 16 +- src/api/seeders/categories.js | 31 ---- src/docs/swagger.json | 150 +++++++++++------- src/helpers/cloudnary.js | 10 -- src/helpers/validators/articleValidator.js | 4 +- src/index.js | 25 +-- src/middlewares/checkArticleOwnership.js | 8 +- src/middlewares/checkAuth.js | 11 -- src/middlewares/checkValidToken.js | 3 +- src/middlewares/errorHandler.js | 12 +- src/middlewares/getOneArticle.js | 8 +- src/middlewares/passport.js | 19 --- src/middlewares/validateArticle.js | 36 ++--- tests/article.js | 98 +++++------- tests/updateArticle.test.js | 18 +-- tests/verify.test.js | 18 +-- 30 files changed, 320 insertions(+), 430 deletions(-) delete mode 100644 src/api/migrations/20190620193548-create-token.js delete mode 100644 src/api/migrations/20190624184203-create-article.js delete mode 100644 src/api/models/token.js delete mode 100644 src/api/routes/articles.js create mode 100644 src/api/seeders/20190623142910-category.js delete mode 100644 src/api/seeders/categories.js delete mode 100644 src/helpers/cloudnary.js delete mode 100644 src/middlewares/checkAuth.js delete mode 100644 src/middlewares/passport.js diff --git a/package.json b/package.json index 35177db2..6d7ce248 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,13 @@ "main": "index.js", "scripts": { "start": "babel-node src/index.js", - "test": "NODE_ENV=test nyc --reporter=html --reporter=text --reporter=lcov ./node_modules/.bin/mocha ./tests/* --require @babel/register --timeout 4000 --exit", + "test": "npm run setup:test && NODE_ENV=test nyc --reporter=html --reporter=text --reporter=lcov ./node_modules/.bin/mocha ./tests/* --require @babel/register --timeout 5000 --exit", "dev": "NODE_ENV=development nodemon --exec babel-node src/index.js", "setup:test": "NODE_ENV=test npm run migrate:undo && NODE_ENV=test npm run migrate && NODE_ENV=test npm run seed", "setup:dev": "NODE_ENV=development npm run migrate:undo && NODE_ENV=development npm run migrate && NODE_ENV=development npm run seed", "pretest": "NODE_ENV=test sequelize db:migrate:undo:all && NODE_ENV=test sequelize db:migrate", "coverage": "nyc report --reporter=text-lcov | coveralls", "eslint": "eslint . --cache --fix", - "setup": "NODE_ENV=test npm run migrate:undo && NODE_ENV=test npm run migrate && NODE_ENV=test npm run seed", "precommit": "lint-staged", "migrate": "node_modules/.bin/sequelize db:migrate", "seed": "node_modules/.bin/sequelize db:seed:all", @@ -55,24 +54,24 @@ "expect": "^24.8.0", "express": "^4.16.4", "express-session": "^1.16.2", + "jsonwebtoken": "^8.5.1", + "lodash": "^4.17.11", + "moment": "^2.24.0", + "morgan": "^1.9.1", + "multer": "^1.4.1", + "passport": "^0.4.0", "passport-facebook": "^3.0.0", "passport-google-oauth": "^2.0.0", + "passport-jwt": "^4.0.0", "passport-pinterest": "^1.0.0", "passport-twitter": "^1.0.4", - "redis": "^2.8.0", - "sinon": "^7.3.2", - "sinon-chai": "^3.3.0", - "jsonwebtoken": "^8.5.1", - "lodash": "^4.17.11", - "morgan": "^1.9.1", "pg": "^7.11.0", "pino": "^5.12.6", "redis": "^2.8.0", "sequelize": "^5.8.7", "sequelize-cli": "^5.5.0", - "moment": "^2.24.0", - "multer": "^1.4.1", - "passport-jwt": "^4.0.0", + "sinon": "^7.3.2", + "sinon-chai": "^3.3.0", "slug": "^1.1.0", "swagger-node-express": "^2.1.3", "swagger-ui-express": "^4.0.6", @@ -90,9 +89,11 @@ "src/api/models", "src/configs/redisConfigs.js", "src/configs/passport.js", + "src/middlewares/passport.js", "src/index.js", "src/middlewares/errorHandler.js", - "src/api/models" + "src/api/models", + "tests/*" ] }, "author": "Andela Simulations Programme", diff --git a/src/api/controllers/articleController.js b/src/api/controllers/articleController.js index 3cc72be2..ec6b54c5 100644 --- a/src/api/controllers/articleController.js +++ b/src/api/controllers/articleController.js @@ -30,31 +30,30 @@ export default class ArticleController { * @returns {Object} res */ static async create(req, res) { - const { id: author } = req.user; - const valid = await articleValidator(req.body); - const { category: name } = req.body; - let image = ''; + const { id: author } = req.user.user; + const articleValidInput = await articleValidator(req.body); + const { category: id } = req.body; + let coverImage; if (req.file) { - const saved = await cloudinary.v2.uploader.upload(req.file.path); - image = saved.secure_url; + const savedFile = await cloudinary.v2.uploader.upload(req.file.path); + coverImage = savedFile.secure_url; } - await Category.findOrCreate({ where: { name } }).spread(async (cat) => { - const { id: category } = cat.get(); - const article = await Article.create({ - ...valid, - slug: '', - author, - category, - image - }); - if (article) { - return res.status(CREATED).json({ - message: errorMessage.articleCreate, - article: article.get() - }); - } + const getCategory = await Category.findOne({ where: { id } }); + const { id: category } = getCategory.dataValues; + const article = await Article.create({ + ...articleValidInput, + slug: '', + author, + category, + coverImage }); + if (article) { + return res.status(CREATED).json({ + message: errorMessage.articleCreate, + article: article.get() + }); + } } /** @@ -89,13 +88,13 @@ export default class ArticleController { tagList: tagList || req.Existing.tagList, category: category || req.Existing.category }; + await Article.update({ slug: updateContent.slug, title: updateContent.title, description: updateContent.description, body: updateContent.body, coverImage, - tagList: updateContent.tagList, category: updateContent.category }, { diff --git a/src/api/controllers/verifyEmail.js b/src/api/controllers/verifyEmail.js index 8a29e0d8..f585946f 100644 --- a/src/api/controllers/verifyEmail.js +++ b/src/api/controllers/verifyEmail.js @@ -8,20 +8,21 @@ import errorMessages from '../../helpers/constants/error.messages'; */ export default class { /** - * @description verification link controller - * @param {*} req - * @param {*} res - * @returns {*} void - */ + * @description verification link controller + * @param {*} req + * @param {*} res + * @returns {*} void + */ static async verifyEmail(req, res) { const { token } = req.params; const result = decodejwt(token); if (result !== undefined) { - const action = await models.User.update({ verified: true }, { - where: { - email: result.user.email, - } - }); + const action = await models.User.update({ verified: true }, + { + where: { + email: result.user.email + } + }); const ZERO = 0; if (!action.includes(ZERO)) { res.status(status.OK).json({ diff --git a/src/api/migrations/20190620193548-create-token.js b/src/api/migrations/20190620193548-create-token.js deleted file mode 100644 index 88868dd7..00000000 --- a/src/api/migrations/20190620193548-create-token.js +++ /dev/null @@ -1,22 +0,0 @@ -export default { - up: (queryInterface, Sequelize) => queryInterface.createTable('Tokens', { - id: { - allowNull: false, - autoIncrement: true, - primaryKey: true, - type: Sequelize.INTEGER - }, - data: { - type: Sequelize.TEXT - }, - createdAt: { - allowNull: false, - type: Sequelize.DATE - }, - updatedAt: { - allowNull: false, - type: Sequelize.DATE - } - }), - down: (queryInterface, Sequelize) => queryInterface.dropTable('Tokens') -}; diff --git a/src/api/migrations/20190622195901-create-category.js b/src/api/migrations/20190622195901-create-category.js index 3534704c..4ba9ef72 100644 --- a/src/api/migrations/20190622195901-create-category.js +++ b/src/api/migrations/20190622195901-create-category.js @@ -1,5 +1,5 @@ export default { - up: (queryInterface, Sequelize) => queryInterface.createTable('categories', { + up: (queryInterface, Sequelize) => queryInterface.createTable('Categories', { id: { allowNull: false, autoIncrement: true, @@ -18,5 +18,5 @@ export default { type: Sequelize.DATE } }), - down: (queryInterface, Sequelize) => queryInterface.dropTable('categories') + down: (queryInterface, Sequelize) => queryInterface.dropTable('Categories') }; diff --git a/src/api/migrations/20190622195902-create-article.js b/src/api/migrations/20190622195902-create-article.js index 451b5d9d..4e3d3822 100644 --- a/src/api/migrations/20190622195902-create-article.js +++ b/src/api/migrations/20190622195902-create-article.js @@ -1,5 +1,5 @@ export default { - up: (queryInterface, Sequelize) => queryInterface.createTable('articles', { + up: (queryInterface, Sequelize) => queryInterface.createTable('Articles', { id: { allowNull: false, autoIncrement: true, @@ -10,7 +10,12 @@ export default { description: { type: Sequelize.TEXT, allowNull: false }, body: { type: Sequelize.TEXT, allowNull: false }, slug: { type: Sequelize.STRING, allowNull: false }, - image: { type: Sequelize.TEXT, allowNull: false }, + coverImage: { type: Sequelize.TEXT, allowNull: false }, + tagList: { + type: Sequelize.ARRAY(Sequelize.TEXT), + allowNull: true + }, + author: { allowNull: false, type: Sequelize.INTEGER, @@ -19,7 +24,7 @@ export default { category: { allowNull: false, type: Sequelize.INTEGER, - references: { model: 'categories', key: 'id' } + references: { model: 'Categories', key: 'id' } }, createdAt: { allowNull: false, @@ -28,7 +33,11 @@ export default { updatedAt: { allowNull: false, type: Sequelize.DATE + }, + deletedAt: { + allowNull: true, + type: Sequelize.DATE } }), - down: (queryInterface, Sequelize) => queryInterface.dropTable('articles') + down: (queryInterface, Sequelize) => queryInterface.dropTable('Articles') }; diff --git a/src/api/migrations/20190624184203-create-article.js b/src/api/migrations/20190624184203-create-article.js deleted file mode 100644 index 6402401c..00000000 --- a/src/api/migrations/20190624184203-create-article.js +++ /dev/null @@ -1,59 +0,0 @@ -export default { - up: (queryInterface, Sequelize) => queryInterface.createTable('Articles', { - id: { - allowNull: false, - unique: true, - primaryKey: true, - type: Sequelize.UUID, - defaultValue: Sequelize.UUIDV4 - }, - title: { - type: Sequelize.STRING, - allowNull: false - }, - slug: { - type: Sequelize.STRING, - allowNull: false, - unique: true - }, - category: { - type: Sequelize.STRING, - allowNull: false, - }, - description: { - type: Sequelize.STRING, - allowNull: false - }, - body: { - type: Sequelize.TEXT, - allowNull: false - }, - coverImage: { - type: Sequelize.TEXT, - allowNull: true - }, - tagList: { - type: Sequelize.ARRAY(Sequelize.TEXT), - allowNull: false - }, - createdAt: { - allowNull: false, - type: Sequelize.DATE - }, - updatedAt: { - allowNull: false, - type: Sequelize.DATE - }, - userId: { - type: Sequelize.INTEGER, - references: { - model: 'Users', - key: 'id' - }, - onDelete: 'CASCADE', - onUpdate: 'CASCADE', - allowNull: false - } - }), - down: (queryInterface, Sequelize) => queryInterface.dropTable('Articles') -}; diff --git a/src/api/models/article.js b/src/api/models/article.js index 4547fdd2..23a8bd6e 100644 --- a/src/api/models/article.js +++ b/src/api/models/article.js @@ -1,8 +1,8 @@ import slug from 'slug'; -const THIRTY_SIX = 36; -const SIX = 6; -const ZERO = 0; +const slugRandomNumberOne = 36; +const slugRandomNumberTwo = 6; +const SlugDefaultNumber = 0; export default (sequelize, DataTypes) => { const Article = sequelize.define('Articles', @@ -18,6 +18,11 @@ export default (sequelize, DataTypes) => { body: { type: DataTypes.TEXT, allowNull: false }, slug: { type: DataTypes.STRING, allowNull: false }, coverImage: { type: DataTypes.TEXT, allowNull: false }, + tagList: { + type: DataTypes.ARRAY(DataTypes.TEXT), + allowNull: true + }, + author: { allowNull: false, type: DataTypes.INTEGER, @@ -26,15 +31,18 @@ export default (sequelize, DataTypes) => { category: { allowNull: false, type: DataTypes.INTEGER, - references: { model: 'categories', key: 'id' } + references: { model: 'Categories', key: 'id' } } }, { - tableName: 'articles', + tableName: 'Articles', + timestamps: true, paranoid: true, hooks: { beforeCreate(article) { - article.slug = slug(`${article.title}-${(Math.random() * THIRTY_SIX ** SIX || ZERO).toString(THIRTY_SIX)}`).toLowerCase(); + article.slug = slug(`${article.title}-${( + Math.random() * slugRandomNumberOne ** slugRandomNumberTwo || SlugDefaultNumber + ).toString(slugRandomNumberOne)}`).toLowerCase(); } } }); diff --git a/src/api/models/category.js b/src/api/models/category.js index dd002228..10d1e3f7 100644 --- a/src/api/models/category.js +++ b/src/api/models/category.js @@ -1,5 +1,5 @@ export default (sequelize, DataTypes) => { - const Category = sequelize.define('Category', + const Category = sequelize.define('Categories', { id: { allowNull: false, @@ -10,7 +10,7 @@ export default (sequelize, DataTypes) => { name: { type: DataTypes.STRING, allowNull: false } }, { - tableName: 'categories', + tableName: 'Categories', hooks: { beforeCreate(category) { category.name = category.name.toUpperCase(); @@ -19,7 +19,7 @@ export default (sequelize, DataTypes) => { }); Category.associate = (models) => { - Category.hasMany(models.Article, { + Category.hasMany(models.Articles, { foreignKey: 'categoryId', as: 'category', onDelete: 'CASCADE', diff --git a/src/api/models/token.js b/src/api/models/token.js deleted file mode 100644 index d08ef765..00000000 --- a/src/api/models/token.js +++ /dev/null @@ -1,8 +0,0 @@ -export default (sequelize, DataTypes) => { - const Token = sequelize.define('Token', - { - data: DataTypes.TEXT - }, - {}); - return Token; -}; diff --git a/src/api/routes/articleRouter.js b/src/api/routes/articleRouter.js index cf5046d1..8e86d20d 100644 --- a/src/api/routes/articleRouter.js +++ b/src/api/routes/articleRouter.js @@ -5,6 +5,7 @@ import checkValidToken from '../../middlewares/checkValidToken'; import bodyVerifier from '../../middlewares/validations/body.verifier'; import checkArticle from '../../middlewares/getOneArticle'; import uploadImage from '../../middlewares/upload'; +import errorHandler from '../../middlewares/errorHandler'; const articleRouter = new Router(); @@ -15,5 +16,9 @@ articleRouter.patch('/:slug', checkArticle.getArticle, checkArticleOwner.checkOwner, articleController.updateArticle); +articleRouter.post('/', + checkValidToken, + uploadImage.single('coverImage'), + errorHandler(articleController.create)); export default articleRouter; diff --git a/src/api/routes/articles.js b/src/api/routes/articles.js deleted file mode 100644 index 03280666..00000000 --- a/src/api/routes/articles.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Router } from 'express'; -import checkAuth from '../../middlewares/checkAuth'; -import ArticleController from '../controllers/articleController'; -import errorHandler from '../../middlewares/errorHandler'; -import uploadImage from '../../middlewares/upload'; - -const articleRouter = new Router(); - -articleRouter - .route('/articles') - .post(checkAuth, uploadImage.single('image'), errorHandler(ArticleController.create)); - -export default articleRouter; diff --git a/src/api/seeders/20190621184055-user.js b/src/api/seeders/20190621184055-user.js index 966ec145..2b92f070 100644 --- a/src/api/seeders/20190621184055-user.js +++ b/src/api/seeders/20190621184055-user.js @@ -8,7 +8,7 @@ module.exports = { up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Users', [ { - username: 'Burindy', + username: 'Burindi', email: 'alain1@gmail.com', password: hashedPassword, createdAt: new Date(), diff --git a/src/api/seeders/20190623142910-category.js b/src/api/seeders/20190623142910-category.js new file mode 100644 index 00000000..220b8550 --- /dev/null +++ b/src/api/seeders/20190623142910-category.js @@ -0,0 +1,38 @@ +module.exports = { + up: queryInterface => queryInterface.bulkInsert('Categories', + [ + { + name: 'LOVE', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'MUSIC', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'ART', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'TECHNOLOGY', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'BUSINESS', + createdAt: new Date(), + updatedAt: new Date() + }, + { + name: 'ENTERTAINMENT', + createdAt: new Date(), + updatedAt: new Date() + } + ], + {}), + + down: queryInterface => queryInterface.bulkDelete('Categories', null, {}) +}; diff --git a/src/api/seeders/20190624184857-Article.js b/src/api/seeders/20190624184857-Article.js index 11a726cf..3f35a670 100644 --- a/src/api/seeders/20190624184857-Article.js +++ b/src/api/seeders/20190624184857-Article.js @@ -2,31 +2,27 @@ export default { up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Articles', [ { - id: 'e6db9e0b-ebdf-468a-9e66-db314b7586c0', title: 'How to create sequalize seeds', slug: 'How-to-create-sequalize-seeds', description: 'How to set dummy data automatically', body: 'Suppose we want to insert some data.', coverImage: 'default.jpeg', - category: 'Tech', - userId: 1, - tagList: ['postgres', 'express', 'sequelize'], + category: 1, + author: 1, createdAt: new Date(), updatedAt: new Date() }, { - id: '263920cd-c7ad-45b0-a5d9-d12f7fe237c3', title: 'What is a Version 1 UUID', slug: 'What-is-a-Version-1-UUID', description: 'Velit non sit culpa pariatur proident', - body:'A Version 1 UUID is a universall', + body: 'A Version 1 UUID is a universall', coverImage: 'default.jpeg', - category: 'Tech', - userId: 1, - tagList: ['UUID', 'express', 'sequelize'], + category: 1, + author: 1, createdAt: new Date(), updatedAt: new Date() - }, + } ], {}), diff --git a/src/api/seeders/categories.js b/src/api/seeders/categories.js deleted file mode 100644 index 337be8a9..00000000 --- a/src/api/seeders/categories.js +++ /dev/null @@ -1,31 +0,0 @@ -const moment = require('moment'); - -const createdAt = moment().format(); -const updatedAt = createdAt; - -module.exports = { - up: queryInterface => queryInterface.bulkInsert('categories', - [ - { - name: 'LOVE', - id: 1, - createdAt, - updatedAt - }, - { - name: 'MUSIC', - id: 2, - createdAt, - updatedAt - }, - { - name: 'ART', - id: 3, - createdAt, - updatedAt - } - ], - {}), - - down: queryInterface => queryInterface.bulkDelete('categories', null, {}) -}; diff --git a/src/docs/swagger.json b/src/docs/swagger.json index 0dbad80c..c55e305d 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -85,9 +85,7 @@ "get": { "tags": ["Users"], "summary": ["Register/login facebook users"], - "description": [ - "Register/login a facebook user and fetch the user profile" - ], + "description": ["Register/login a facebook user and fetch the user profile"], "produces": ["application/json"], "responses": { "200": { @@ -106,9 +104,7 @@ "get": { "tags": ["Users"], "summary": ["Register/login google users "], - "description": [ - "Register/login a google user and fetch the user profile" - ], + "description": ["Register/login a google user and fetch the user profile"], "produces": ["application/json"], "responses": { "200": { @@ -127,9 +123,7 @@ "get": { "tags": ["Users"], "summary": ["Register/login twitter users"], - "description": [ - "Register/login a twitter user and fetch the user profile" - ], + "description": ["Register/login a twitter user and fetch the user profile"], "produces": ["application/json"], "responses": { "200": { @@ -244,69 +238,112 @@ } }, "/users/signout": { - "get":{ - "tags":["auth"], - "summary":"signs out the logged in user", - "description":"drop the token", - "produces":["application/json"], - "parameters":[ - { - "in":"header", - "name":"authorization", - "description":"token to be dropped", - "required":true - } + "get": { + "tags": ["auth"], + "summary": "signs out the logged in user", + "description": "drop the token", + "produces": ["application/json"], + "parameters": [ + { + "in": "header", + "name": "authorization", + "description": "token to be dropped", + "required": true + } ], - "responses":{ - "200":{ - "description":"You are signed out" + "responses": { + "200": { + "description": "You are signed out" }, - "401":{ - "description":"You are unauthorized, Token is no longer valid" + "401": { + "description": "You are unauthorized, Token is no longer valid" }, - "403":{ - "description":"Forbiden access" + "403": { + "description": "Forbiden access" + } + }, + "security": [ + { + "api_key": [] + } + ] } - }, - "security": [ + }, + "/articles": { + "post": { + "tags": ["Articles"], + "summary": "Create article", + "description": "create new article", + "produces": ["application/json"], + "consumes": ["application/json"], + "parameters": [ { - "api_key":[] + "in": "header", + "name": "authorization", + "description": "user token to verify", + "required": true + }, + { + "in": "path", + "name": "slug", + "description": "articles URL", + "required": true + }, + { + "in": "body", + "name": "body", + "description": "field required to create article", + "required": true } - ] - } - }, - "/articles/{slug}": { - "patch": { - "tags": ["Articles"], + ], + "response": { + "201": { + "description": "Article created succesfully" + }, + "404": { + "description": "Not found" + }, + "403": { + "description": "Forbiden access" + }, + "401": { + "description": "Forbiden access" + } + } + } + }, + "/articles/{slug}": { + "patch": { + "tags": ["Articles"], "summary": "Update the article based on its current s-l-u-g", "description": "update article by slug", "produces": ["application/json"], "consumes": ["application/json"], - "parameters":[ + "parameters": [ { - "in":"header", - "name":"authorization", - "description":"user token to verify", - "required":true + "in": "header", + "name": "authorization", + "description": "user token to verify", + "required": true }, { - "in":"path", - "name":"slug", - "description":"slug to be used in updating the article", - "required":true + "in": "path", + "name": "slug", + "description": "slug to be used in updating the article", + "required": true }, { - "in":"body", - "name":"body", - "description":"update all of the body or any property among them", - "required":true + "in": "body", + "name": "body", + "description": "update all of the body or any property among them", + "required": true } ], "response": { "200": { "description": "Your Article is up-to-date now, Thanks" }, - "404":{ + "404": { "description": "Article with this { slug } is not found, Thanks" }, "403": { @@ -316,8 +353,8 @@ "description": "Please you must be the owner of this Article in order to modify it, Thanks" } } - } } + } }, "externalDocs": { "description": "Find out more about Swagger", @@ -405,26 +442,25 @@ } }, "article": { - "type":"object", + "type": "object", "properties": { "title": { - "type":"string" + "type": "string" }, "description": { - "type":"string" + "type": "string" }, "body": { "type": "string" }, "tagList": { - "type":"string" + "type": "string" }, "category": { "type": "string" } } } - } } } diff --git a/src/helpers/cloudnary.js b/src/helpers/cloudnary.js deleted file mode 100644 index 8945e841..00000000 --- a/src/helpers/cloudnary.js +++ /dev/null @@ -1,10 +0,0 @@ -import dotenv from 'dotenv'; -import cloudinary from 'cloudinary'; - -dotenv.config(); - -cloudinary.config({ - cloud_name: process.env.API_CLOUD_NAME, - api_key: process.env.API_KEY, - api_secret: process.env.API_SECRET -}); diff --git a/src/helpers/validators/articleValidator.js b/src/helpers/validators/articleValidator.js index b1d2f01e..29c4eb86 100644 --- a/src/helpers/validators/articleValidator.js +++ b/src/helpers/validators/articleValidator.js @@ -1,7 +1,7 @@ import joi from '@hapi/joi'; import errorMessage from '../constants/error.messages'; -const schema = { +const schema = joi.object().keys({ title: joi .string() .error(() => errorMessage.title) @@ -19,6 +19,6 @@ const schema = { .required() .uppercase() .error(() => errorMessage.category) -}; +}); export default article => joi.validate(article, schema); diff --git a/src/index.js b/src/index.js index 593140e2..539f9264 100644 --- a/src/index.js +++ b/src/index.js @@ -1,35 +1,18 @@ import express from 'express'; -import passport from 'passport'; -import session from 'express-session'; -import cors from 'cors'; -import logger from 'morgan'; import apiRouter from './api/routes/index'; import docsRouter from './api/routes/docs'; import homeRouter from './api/routes/home'; +import register from './middlewares/register.app'; import env from './configs/environments'; -import passportConfig from './middlewares/passport'; -import './helpers/cloudnary'; - const app = express(); -app.use(cors()); -app.use(express.json()); -app.use(express.urlencoded({ extended: false })); -app.use(logger('dev')); - -app.use(express.static(`${__dirname}/public`)); -app.use(passport.initialize()); -passportConfig(passport); -app.use(session({ - secret: env.secret, - cookie: { maxAge: 60000 }, - resave: false, - saveUninitialized: false -})); +// Register middleware +register(app); app.use('/api', apiRouter); app.use('/docs', docsRouter); + app.use('/', homeRouter); app.listen(env.appPort); diff --git a/src/middlewares/checkArticleOwnership.js b/src/middlewares/checkArticleOwnership.js index 70ea9173..5c66b163 100644 --- a/src/middlewares/checkArticleOwnership.js +++ b/src/middlewares/checkArticleOwnership.js @@ -21,13 +21,13 @@ class CheckUserOwnership { * @returns {Object} res */ static async checkOwner(req, res, next) { - const { user: { id } } = req.user; + const { id: author } = req.user.user; const { slug } = req.params; const response = await Article.findOne({ where: { slug, - userId: id + author } }); @@ -35,7 +35,9 @@ class CheckUserOwnership { req.Existing = response.dataValues; next(); } else { - res.status(statusCode.ACCESS_DENIED).json({ message: 'Please you must be the owner of this Article in order to modify it, Thanks' }); + res.status(statusCode.ACCESS_DENIED).json({ + message: 'Please you must be the owner of this Article in order to modify it, Thanks' + }); } } } diff --git a/src/middlewares/checkAuth.js b/src/middlewares/checkAuth.js deleted file mode 100644 index da445fbd..00000000 --- a/src/middlewares/checkAuth.js +++ /dev/null @@ -1,11 +0,0 @@ -import passport from 'passport'; -import statusCodes from '../helpers/constants/status.codes'; -import errorMessages from '../helpers/constants/error.messages'; - -const { UNAUTHORIZED } = statusCodes; -const { authenticationMessage } = errorMessages; -export default (req, res, next) => passport.authenticate('jwt', { session: false }, (err, user) => { - if (!user) return res.status(UNAUTHORIZED).json({ message: authenticationMessage }); - req.user = user.dataValues; - return next(); -})(req, res, next); diff --git a/src/middlewares/checkValidToken.js b/src/middlewares/checkValidToken.js index 767593f2..5c3e8408 100644 --- a/src/middlewares/checkValidToken.js +++ b/src/middlewares/checkValidToken.js @@ -28,7 +28,8 @@ const validToken = async (req, res, next) => { } catch (err) { res.status(statuses.UNAUTHORIZED).json({ status: statuses.UNAUTHORIZED, - message: 'Forbiden access' + message: 'Forbiden access', + error: `${err}` }); } }; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index 3070c3a2..04b81890 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -1,17 +1,15 @@ import statusCodes from '../helpers/constants/status.codes'; -import errorMessages from '../helpers/constants/error.messages'; +import errorSender from '../helpers/error.sender'; const { BAD_REQUEST, SERVER_ERROR } = statusCodes; -const { serverError } = errorMessages; export default callback => async (req, res, next) => { try { await callback(req, res, next); } catch (error) { - console.log(error); - const { parent, details } = error; - if (parent) return res.status(SERVER_ERROR).json({ message: parent.detail }); - if (details) return res.status(BAD_REQUEST).json({ message: details[0].message }); - return res.status(SERVER_ERROR).json({ message: serverError }); + const { parent, isJoi = false, details } = error; + if (parent) return errorSender(SERVER_ERROR, res, 'internal server error', parent.detail); + if (isJoi) return errorSender(BAD_REQUEST, res, 'internal server error', details[0].message); + return res.status(SERVER_ERROR).json(error); } }; diff --git a/src/middlewares/getOneArticle.js b/src/middlewares/getOneArticle.js index c4299b0c..74142f15 100644 --- a/src/middlewares/getOneArticle.js +++ b/src/middlewares/getOneArticle.js @@ -25,11 +25,15 @@ class getOneArticle { const result = await Article.findOne({ where: { - slug, + slug } }); - return result ? next() : res.status(statusCode.NOT_FOUND).json({ message: `Article with this ${slug} is not found, Thanks` }); + return result + ? next() + : res + .status(statusCode.NOT_FOUND) + .json({ message: `Article with this ${slug} is not found, Thanks` }); } } diff --git a/src/middlewares/passport.js b/src/middlewares/passport.js deleted file mode 100644 index 9e808ec9..00000000 --- a/src/middlewares/passport.js +++ /dev/null @@ -1,19 +0,0 @@ -import { Strategy, ExtractJwt } from 'passport-jwt'; -import models from '../api/models'; -import env from '../configs/environments'; - -const { User } = models; -export default (passport) => { - passport.use(new Strategy({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: env.secret - }, - async ({ user: userData = {} }, done) => { - try { - const user = await User.findOne({ where: { id: userData.id } }); - done(null, user || false); - } catch (error) { - done(error); - } - })); -}; diff --git a/src/middlewares/validateArticle.js b/src/middlewares/validateArticle.js index 39a25775..c03aefc5 100644 --- a/src/middlewares/validateArticle.js +++ b/src/middlewares/validateArticle.js @@ -2,26 +2,22 @@ import articleValidator from '../helpers/validators/articleValidator'; import statusCode from '../helpers/constants/status.codes'; /** - * @class + * @param {object} req The request object containing method, body, params, query,...ß + * @param {object} res The response object for the request + * @param {function} next The next middleware function in the request pipeline + * @returns {object} The response with status and data or error message */ -export default class { - /** - * @param {object} req The request object containing method, body, params, query,...ß - * @param {object} res The response object for the request - * @param {function} next The next middleware function in the request pipeline - * @returns {object} The response with status and data or error message - */ - static async validateArticle(req, res, next) { - try { - await articleValidator(req.body); - return next(); - } catch (error) { - if (error.isJoi) { - const [err] = error.details; - const { message } = err; - return res.status(statusCode.BAD_REQUEST).json({ message }); - } - return res.status(statusCode.BAD_REQUEST).json({ error }); +export default async (req, res, next) => { + // return res.status(statusCode.BAD_REQUEST).json({ error: 'jjjjj' }); + try { + await articleValidator(req.body); + return next(); + } catch (error) { + if (error.isJoi) { + const [err] = error.details; + const { message } = err; + return res.status(statusCode.BAD_REQUEST).json({ message, details: error.details }); } + return res.status(statusCode.BAD_REQUEST).json({ error }); } -} +}; diff --git a/tests/article.js b/tests/article.js index 35829c28..f086602d 100644 --- a/tests/article.js +++ b/tests/article.js @@ -2,12 +2,11 @@ import chai from 'chai'; import '@babel/polyfill'; import chaiHttp from 'chai-http'; import statusCode from '../src/helpers/constants/status.codes'; -import errorMessages from '../src/helpers/constants/error.messages'; import server from '../src/index'; chai.use(chaiHttp); chai.should(); -let userToken = ''; +let userToken; const { expect } = chai; const fakeArticle = { @@ -16,7 +15,7 @@ const fakeArticle = { ' how did the classical Latin become so incoherent? According to McClintock, a 15th century typesetter likely scrambled part o', body: 'po how did the classical Latin become so incoherent? According to McClintock, a 15th century typesetter likely scrambled part of Ciceros De Finibus in order to provide placeholder text to mockup various fonts for a type specimen book.It is difficult to find examples of lorem ipsum in use before Letraset made it popular as a dummy text in the 1960s, although McClintock says he remembers coming across the lorem ipsum passage in a book of old metal type samples. So far he hasnt relocated where he once saw the passage, but the popularity of Cicero in the 15th century supports the theory that the filler text has been used for centuries.d anyways, as Cecil Adams reasoned, “[Do you really] think graphic arts supply houses were hiring classics scholars in the 1960s?” Perhaps. But it seems reasonable to imagine that there was a version in use far before the age of Letraset', - category: 'arts' + category: 1 }; describe('Testing the authorization', () => { it('should return an error if the user has not logged in', (done) => { @@ -28,12 +27,15 @@ describe('Testing the authorization', () => { .end((err, res) => { const { status, body } = res; expect(status).to.equal(statusCode.UNAUTHORIZED); - expect(body).to.have.property('message', errorMessages.authenticationMessage); + + expect(body).to.have.property('message'); + expect(body.message).to.eql('Forbiden access'); done(); }); }); }); -describe('create article', () => { + +describe('create article', () => { const user = { username: 'dianeMurekatete', email: 'diane@gmail.com', @@ -47,74 +49,63 @@ describe('create article', () => { .post('/api/users/signup') .send(user) .end((err, res) => { - console.log('res.data ===>', res.body.user.token); - res.should.have.status(statusCode.CREATED); - res.body.should.be.an('Object'); userToken = res.body.user.token; - done(); }); }); - it('should create an article', (done) => { + it('should post an article ', (done) => { const { ...rest } = fakeArticle; chai .request(server) .post('/api/articles') - .set('Authorization', `Bearer ${userToken}`) + .set('authorization', userToken) .field(rest) - .attach('image', './tests/images/image.jpeg') + .attach('coverImage', './tests/images/image.jpeg', 'image') .end((err, res) => { - const { status, body } = res; + const { status } = res; expect(status).to.equal(statusCode.CREATED); - expect(body).to.have.property('message', errorMessages.articleCreate); done(); }); }); - - describe('validate article', () => { - it('should not post an article without title ', (done) => { - const { title, ...rest } = fakeArticle; - chai - .request(server) - .post('/api/articles') - .set('Authorization', `Bearer ${userToken}`) - .field(rest) - .attach('image', './tests/images/image.jpeg') - .end((err, res) => { - const { status, body } = res; - expect(status).to.equal(statusCode.BAD_REQUEST); - expect(body).to.have.property('message', errorMessages.title); - done(); - }); - }); - it('should not post an article without description', (done) => { - const { description, ...rest } = fakeArticle; - chai - .request(server) - .post('/api/articles') - .set('Authorization', `Bearer ${userToken}`) - .field(rest) - .attach('image', './tests/images/image.jpeg') - .end((err, res) => { - const { status, body } = res; - expect(status).to.equal(statusCode.BAD_REQUEST); - expect(body).to.have.property('message', errorMessages.description); - done(); - }); - }); + it('should not post an article without title ', (done) => { + const { title, ...rest } = fakeArticle; + chai + .request(server) + .post('/api/articles') + .set('authorization', userToken) + .send(rest) + .end((err, res) => { + const { status, body } = res; + expect(status).to.equal(statusCode.BAD_REQUEST); + expect(body).to.have.property('errors'); + done(); + }); + }); + it('should not post an article without description', (done) => { + const { description, ...rest } = fakeArticle; + chai + .request(server) + .post('/api/articles') + .set('authorization', userToken) + .send(rest) + .end((err, res) => { + const { status, body } = res; + expect(status).to.equal(statusCode.BAD_REQUEST); + expect(body).to.have.property('errors'); + done(); + }); }); it('should not post an article without body', (done) => { const { body: b, ...rest } = fakeArticle; chai .request(server) .post('/api/articles') - .set('Authorization', `Bearer ${userToken}`) - .field(rest) - .attach('image', './tests/images/image.jpeg') + .set('authorization', userToken) + .send(rest) .end((err, res) => { const { status, body } = res; expect(status).to.equal(statusCode.BAD_REQUEST); - expect(body).to.have.property('message', errorMessages.body); + expect(body).to.have.property('errors'); done(); }); }); @@ -123,13 +114,12 @@ describe('create article', () => { chai .request(server) .post('/api/articles') - .set('Authorization', `Bearer ${userToken}`) - .field(rest) - .attach('image', './tests/images/image.jpeg') + .set('authorization', userToken) + .send(rest) .end((err, res) => { const { status, body } = res; expect(status).to.equal(statusCode.BAD_REQUEST); - expect(body).to.have.property('message', errorMessages.category); + expect(body).to.have.property('errors'); done(); }); }); diff --git a/tests/updateArticle.test.js b/tests/updateArticle.test.js index d2251fa0..c4779f79 100644 --- a/tests/updateArticle.test.js +++ b/tests/updateArticle.test.js @@ -6,7 +6,7 @@ import statuses from '../src/helpers/constants/status.codes'; const { expect } = chai; chai.use(chaiHttp); -let UserToken = ''; +let UserToken; describe('testing the middlewares before reaching the update article controller', () => { it('should signup a user to be checking against', (done) => { @@ -41,9 +41,9 @@ describe('testing the middlewares before reaching the update article controller' chai .request(app) .patch(`/api/articles/${slug}`) - .set('Authorization', UserToken) + .set('authorization', UserToken) .send({ - tagList: ['Yoga', 'Health'] + body: ['how did the classical Latin become'] }) .end((err, res) => { expect(res.status).eql(statuses.NOT_FOUND); @@ -59,9 +59,9 @@ describe('testing the middlewares before reaching the update article controller' chai .request(app) .patch(`/api/articles/${slug}`) - .set('Authorization', UserToken) + .set('authorization', UserToken) .send({ - tagList: ['sequelize', 'postgres', 'controllers'] + body: ['how did the classical Latin become'] }) .end((err, res) => { expect(res.status).eql(statuses.ACCESS_DENIED); @@ -101,9 +101,9 @@ describe('testing testing testing and testing for the article update controller' chai .request(app) .patch(`/api/articles/${slug}`) - .set('Authorization', UserToken) + .set('authorization', UserToken) .field({ - tagList: ['sequelize', 'postgres', 'controllers'] + body: ['how did the classical Latin become'] }) .attach('coverImage', './tests/images/eric.jpg', 'eric.jpg') .end((err, res) => { @@ -120,9 +120,9 @@ describe('testing testing testing and testing for the article update controller' chai .request(app) .patch(`/api/articles/${slug}`) - .set('Authorization', UserToken) + .set('authorization', UserToken) .send({ - tagList: '' + body: '' }) .end((err, res) => { expect(res.status).eql(statuses.OK); diff --git a/tests/verify.test.js b/tests/verify.test.js index 15965439..8d8d9b11 100644 --- a/tests/verify.test.js +++ b/tests/verify.test.js @@ -6,18 +6,15 @@ import server from '../src/index'; import status from '../src/helpers/constants/status.codes'; import errorMessages from '../src/helpers/constants/error.messages'; - chai.use(chaihttp); chai.should(); - describe('GET /verifyEmail', () => { it('Should return a success message if the link is valid', (done) => { const user = { - username: 'BurindiAlain', - email: 'alain@gmail.com', - password: 'password23423', - confirmPassword: 'password23423' + username: 'Burindi', + email: 'alain1@gmail.com', + password: 'password23423' }; const { username, email } = user; @@ -33,7 +30,7 @@ describe('GET /verifyEmail', () => { }); }); - it('Should return a message if a user doesn\'t exist', (done) => { + it("Should return a message if a user doesn't exist", (done) => { const user = { username: 'BurindiAlain', email: 'alain@gmail.com', @@ -62,10 +59,9 @@ describe('GET /verifyEmail', () => { it('Should respond with a message if the token is invalid', (done) => { const user = { - username: 'BurindiAlain', - email: 'alain@gmail.com', - password: 'password23423', - confirmPassword: 'password23423' + username: 'Burindi', + email: 'alain1@gmail.com', + password: 'password23423' }; chai