diff --git a/.travis.yml b/.travis.yml index 31e327e7..fda22fac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,3 +15,4 @@ script: - npm test after_success: - npm run coverage + \ No newline at end of file diff --git a/package.json b/package.json index 3e182150..85ee5999 100755 --- a/package.json +++ b/package.json @@ -81,7 +81,8 @@ "nyc": "^13.1.0", "rimraf": "^2.6.3", "sequelize-cli": "^5.4.0", - "sinon": "^7.2.2" + "sinon": "^7.2.2", + "sinon-chai": "^3.3.0" }, "nyc": { "require": [ @@ -97,4 +98,4 @@ "/server/test/*.js`" ] } -} +} \ No newline at end of file diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 617bc1a0..0d26de1c 100755 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -3,6 +3,7 @@ import MailManager from '../helpers/MailManager'; import db from '../models'; import PasswordManager from '../helpers/PasswordManager'; + const { User } = db; /** @@ -21,7 +22,11 @@ class UserController { static async forgotPassword(req, res) { try { const { email } = req.body; - const user = await User.findOne({ where: { email } }); + const user = await User.findOne({ + where: { + email + } + }); if (!user) { return res.status(404).json({ status: 'failure', @@ -50,6 +55,7 @@ class UserController { statusCode: 200, message: 'Kindly check your mail to reset your password' } + }); } catch (error) { res.status(500).send({ @@ -102,8 +108,14 @@ class UserController { } await User.update( - { password: PasswordManager.hashPassword(newPassword) }, - { where: { email } } + { + password: PasswordManager.hashPassword(newPassword) + }, + { + where: { + email + } + } ); res.status(200).json({ @@ -114,7 +126,10 @@ class UserController { } }); } catch (error) { - if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + if ( + error.name === 'TokenExpiredError' + || error.name === 'JsonWebTokenError' + ) { return res.status(401).send({ status: 'failure', data: { diff --git a/server/controllers/followController.js b/server/controllers/followController.js new file mode 100644 index 00000000..1abb73cf --- /dev/null +++ b/server/controllers/followController.js @@ -0,0 +1,231 @@ +import db from '../models'; +import response from '../helpers/response'; + +const { User, Follow } = db; + +/** + * + * + * @class followContoller + */ +class followContoller { + /** + * + * @description Method to follow user + * @static + * @param {*} req + * @param {*} res + * @returns {object} Json response + * @memberof followContoller + */ + static async followUser(req, res) { + try { + const { userName } = req.params; + const user = await User.findOne({ + where: { + userName + } + }); + + if (!user) { + return response(res, 404, 'failure', 'User not found'); + } + + const userId = user.id; + const followersId = req.user.userId; + + if (user.id === req.user.userId) { + return response(res, 400, 'failure', 'You cannot follow yourself'); + } + + Follow.findOrCreate({ + where: { userId, followersId }, + attributes: ['id', 'followersId', 'userId'] + }).spread((follow, created) => { + if (created) { + return response( + res, + 201, + 'success', + `You are now following ${user.userName}` + ); + } + return response( + res, + 400, + 'failure', + `You are already following ${user.userName}` + ); + }); + } catch (error) { + response(res, 500, 'failure', error.name); + } + } + + /** + * + * @description Method to unfollow user + * @static + * @param {*} req + * @param {*} res + * @returns {object} Json response + * @memberof followContoller + */ + static async unfollowUser(req, res) { + try { + const { userName } = req.params; + + const user = await User.findOne({ + where: { + userName + } + }); + + if (!user) { + return response(res, 404, 'failure', 'User not found'); + } + + const userId = user.id; + + const followersId = req.user.userId; + + if (user.id === req.user.userId) { + return response(res, 400, 'failure', 'You cannot unfollow yourself'); + } + + const userUnfollow = await Follow.findOne({ + where: { userId, followersId } + }); + + if (!userUnfollow) { + return response( + res, + 400, + 'failure', + `You are not following ${user.userName}` + ); + } + + userUnfollow.destroy(); + response(res, 200, 'success', `You unfollowed ${user.userName}`); + } catch (error) { + response(res, 500, 'failure', error.name); + } + } + + /** + * + * @description Method to get all followers + * @static + * @param {*} req + * @param {*} res + * @returns {object} Json response + * @memberof followContoller + */ + static async getFollowers(req, res) { + try { + const { userName } = req.params; + const user = await User.findOne({ + where: { userName } + }); + + if (!user) { + return response(res, 404, 'failure', 'User not found'); + } + + const userId = user.dataValues.id; + + const followers = await Follow.findAll({ + where: { userId }, + include: [ + { + model: User, + as: 'followingUser', + attributes: { exclude: ['email', 'password', 'isVerified', 'notifySettings', 'roleId', 'authTypeId', 'createdAt', 'updatedAt'] } + } + ], + + }); + + if (followers.length === 0) { + return response(res, 200, 'success', 'You currenly have no followers'); + } + + const payload = { + followers: followers.map(follower => follower.followingUser), + get followCount() { + return this.followers.length; + } + }; + + response( + res, + 200, + 'success', + 'Followers returned successfully', + null, + payload + ); + } catch (error) { + response(res, 500, 'failure', error.name); + } + } + + /** + * + * @description Method to get all following + * @static + * @param {*} req + * @param {*} res + * @returns {object} Json response + * @memberof followContoller + */ + static async getFollowing(req, res) { + try { + const { userName } = req.params; + const user = await User.findOne({ + where: { userName } + }); + + if (!user) { + return response(res, 404, 'failure', 'User not found'); + } + + const userId = user.dataValues.id; + + const following = await Follow.findAll({ + where: { followersId: userId }, + attributes: [], + include: [ + { + model: User, + as: 'followedUser', + attributes: { exclude: ['email', 'password', 'isVerified', 'notifySettings', 'roleId', 'authTypeId', 'createdAt', 'updatedAt'] } + } + ] + }); + + if (following.length === 0) { + return response(res, 200, 'success', 'You are not following anyone'); + } + const payload = { + following: following.map(followingUser => followingUser.followedUser), + get followCount() { + return this.following.length; + } + }; + + response( + res, + 200, + 'success', + 'Following returned successfully', + null, + payload + ); + } catch (error) { + response(res, 500, 'failure', error.name); + } + } +} +export default followContoller; diff --git a/server/helpers/response.js b/server/helpers/response.js index 9c2651f8..e41c0bea 100644 --- a/server/helpers/response.js +++ b/server/helpers/response.js @@ -1,14 +1,15 @@ /** - * - * @description Method to send response in a generic format. - * @param {*} res Express Response object - * @param {number} code HTTP response status code - * @param {string} status 'success' || 'failure' - * @param {string} message Message to user - * @param {object} error (optional) Error object - * @param {object} payload (optional) Payload data to return with the response - * @returns {object} Json response - */ + + * + * @description Method to send response in a generic format. + * @param {*} res Express Response object + * @param {number} code HTTP response status code + * @param {string} status 'success' || 'failure' + * @param {string} message Message to user + * @param {object} error (optional) Error object + * @param {object} payload (optional) Payload data to return with the response + * @returns {object} Json response + */ export default (res, code, status, message, error, payload) => { res.status(code).json({ diff --git a/server/middlewares/AuthMiddleware.js b/server/middlewares/AuthMiddleware.js index 0346a797..1b89f618 100755 --- a/server/middlewares/AuthMiddleware.js +++ b/server/middlewares/AuthMiddleware.js @@ -18,9 +18,10 @@ class AuthMiddleware { const { authorization } = req.headers; if (!authorization) { return response( - res, 401, 'failure', 'authentication error', - { message: 'You are not logged in.' }, - null + res, + 401, + 'failure', + 'You are not logged in.' ); } @@ -35,16 +36,19 @@ class AuthMiddleware { const { name } = error; if (name === 'TokenExpiredError' || name === 'JsonWebTokenError') { return response( - res, 401, 'failure', 'authentication error', - { message: 'Token is invalid, You need to log in again' }, - null + res, + 401, + 'failure', + 'Token is invalid, You need to log in again' ); } return response( - res, 500, 'failure', - 'server error', - { message: 'Something went wrong on the server' }, null + res, + 500, + 'failure', + 'An error occured on the server', + error ); } } diff --git a/server/migrations/20190121154422-create-user-follows.js b/server/migrations/20190121154422-create-user-follows.js new file mode 100644 index 00000000..2ba3eed4 --- /dev/null +++ b/server/migrations/20190121154422-create-user-follows.js @@ -0,0 +1,38 @@ +'use strict'; +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable('UserFollows', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + followersId: { + type: Sequelize.UUID, + references: { + model: 'Users', + key: 'id' + } + }, + usersId: { + type: Sequelize.UUID, + references: { + model: 'Users', + key: 'id' + } + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + down: (queryInterface, Sequelize) => { + return queryInterface.dropTable('UserFollows'); + } +}; diff --git a/server/models/follow.js b/server/models/follow.js index 29814213..4a2252a9 100755 --- a/server/models/follow.js +++ b/server/models/follow.js @@ -13,5 +13,19 @@ export default (sequelize, DataTypes) => { }, {} ); + + Follow.associate = (models) => { + const { User } = models; + Follow.belongsTo(User, { + foreignKey: 'userId', + onDelete: 'CASCADE', + as: 'followedUser' + }); + Follow.belongsTo(User, { + foreignKey: 'followersId', + onDelete: 'CASCADE', + as: 'followingUser' + }); + }; return Follow; }; diff --git a/server/models/user.js b/server/models/user.js index bbf19880..ac86f8fe 100755 --- a/server/models/user.js +++ b/server/models/user.js @@ -58,6 +58,7 @@ export default (sequelize, DataTypes) => { type: DataTypes.UUID, allowNull: false, defaultValue: '3ceb546e-054d-4c1d-8860-e27c209d4ae3' + }, authTypeId: { type: DataTypes.UUID, @@ -119,6 +120,7 @@ export default (sequelize, DataTypes) => { through: 'Rating', foreignKey: 'userId' }); + User.belongsToMany(User, { through: 'Follow', as: 'followers', diff --git a/server/models/userFollows.js b/server/models/userFollows.js new file mode 100644 index 00000000..ecfbc20e --- /dev/null +++ b/server/models/userFollows.js @@ -0,0 +1,11 @@ +export default (sequelize, DataTypes) => { + const UserFollow = sequelize.define( + 'UserFollow', + { + followersId: DataTypes.UUID, + usersId: DataTypes.UUID + }, + {} + ); + return UserFollow; +}; diff --git a/server/routes/api/user.js b/server/routes/api/user.js index 60ed3e60..a6614088 100755 --- a/server/routes/api/user.js +++ b/server/routes/api/user.js @@ -1,6 +1,10 @@ import { Router } from 'express'; import UserController from '../../controllers/UserController'; + import userValidator from '../../middlewares/validations/userValidator'; +import followController from '../../controllers/followController'; +import AuthMiddleware from '../../middlewares/AuthMiddleware'; + const userRoutes = Router(); @@ -10,4 +14,20 @@ userRoutes.post('/password/reset/:token', UserController.passwordReset); userRoutes.post('/auth/signup', userValidator.validateUserSignupInput, UserController.signUp); userRoutes.post('/auth/login', userValidator.validateUserLoginInput, UserController.logIn); +userRoutes.get('/users/:userName/followers', followController.getFollowers); + +userRoutes.get('/users/:userName/following', followController.getFollowing); + +userRoutes.post( + '/users/:userName/follow', + AuthMiddleware.checkIfUserIsAuthenticated, + followController.followUser +); + +userRoutes.delete( + '/users/:userName/unfollow', + AuthMiddleware.checkIfUserIsAuthenticated, + followController.unfollowUser +); + export default userRoutes; diff --git a/server/seeders/20190115151310-demoUser.js b/server/seeders/20190115151310-demoUser.js index 324dee2e..ae5b9384 100755 --- a/server/seeders/20190115151310-demoUser.js +++ b/server/seeders/20190115151310-demoUser.js @@ -7,7 +7,7 @@ export default { bio: 'Gitting Started', password: 'Blahblah', authTypeId: '15745c60-7b1a-11e8-9c9c-2d42b21b1a3e' - },{ + }, { id: 'aba396bd-7ac4-42c3-b442-cf10dd73e4f4', fullName: 'Kabir Alausa', userName: 'kabir', @@ -25,6 +25,6 @@ export default { password: 'Blahblah', authTypeId: '15745c60-7b1a-11e8-9c9c-2d42b21b1a3e' } -], {}), + ], {}), down: (queryInterface, Sequelize) => queryInterface.bulkDelete('Users', null, {}) }; diff --git a/server/test/controllers/articles.spec.js b/server/test/controllers/articles.spec.js index 257b0e21..6267fb41 100644 --- a/server/test/controllers/articles.spec.js +++ b/server/test/controllers/articles.spec.js @@ -13,7 +13,7 @@ chai.use(chaiHttp); chai.should(); describe('API endpoint /articles/', () => { - let newArticleSlug; + let newArticleSlug; describe('POST an article', () => { @@ -32,10 +32,10 @@ describe('API endpoint /articles/', () => { it('It should not allow duplication of article content', async () => { const response = await chai - .request(app) - .post('/api/v1/articles') - .set({ authorization: `Bearer ${userToken}` }) - .send(mockArticles[1]); + .request(app) + .post('/api/v1/articles') + .set({ authorization: `Bearer ${userToken}` }) + .send(mockArticles[1]); expect(response.status).to.eqls(409); expect(response.body.status).to.eqls('failure'); @@ -44,10 +44,10 @@ describe('API endpoint /articles/', () => { it('It should check for bad banner url', async () => { const response = await chai - .request(app) - .post('/api/v1/articles') - .set({ authorization: `Bearer ${userToken}` }) - .send(mockArticles[2]); + .request(app) + .post('/api/v1/articles') + .set({ authorization: `Bearer ${userToken}` }) + .send(mockArticles[2]); expect(response.status).to.eqls(400); expect(response.body.status).to.eqls('failure'); @@ -56,33 +56,33 @@ describe('API endpoint /articles/', () => { it('It should not allow unauthenticated users to post article', async () => { const response = await chai - .request(app) - .post('/api/v1/articles') - .set({ authorization: `Bearer ${invalidToken}` }) - .send(mockArticles[1]); + .request(app) + .post('/api/v1/articles') + .set({ authorization: `Bearer ${invalidToken}` }) + .send(mockArticles[1]); expect(response.status).to.eqls(401); expect(response.body.status).to.eqls('failure'); - expect(response.body.data.message).to.eqls('authentication error'); + expect(response.body.data.message).to.eqls('Token is invalid, You need to log in again'); }); it('It should not allow user with invalid token to post an article', async () => { const response = await chai - .request(app) - .post('/api/v1/articles') - .send(mockArticles[1]); + .request(app) + .post('/api/v1/articles') + .send(mockArticles[1]); expect(response.status).to.eqls(401); expect(response.body.status).to.eqls('failure'); - expect(response.body.data.message).to.eqls('authentication error'); + expect(response.body.data.message).to.eqls('You are not logged in.'); }); it('It should show validation error if empty data is passed', async () => { const response = await chai - .request(app) - .post('/api/v1/articles') - .set({ authorization: `Bearer ${userToken}` }) - .send(mockArticles[0]); + .request(app) + .post('/api/v1/articles') + .set({ authorization: `Bearer ${userToken}` }) + .send(mockArticles[0]); expect(response.status).to.eqls(400); expect(response.body.status).to.eqls('failure'); @@ -188,13 +188,13 @@ describe('API endpoint /articles/', () => { it('It should not allow unauthenticated users to update article', async () => { const response = await chai - .request(app) - .put('/api/v1/articles/jwt-key-use-case-2') - .send(mockArticles[4]); + .request(app) + .put('/api/v1/articles/jwt-key-use-case-2') + .send(mockArticles[4]); expect(response.status).to.eqls(401); expect(response.body.status).to.eqls('failure'); - expect(response.body.data.message).to.eqls('authentication error'); + expect(response.body.data.message).to.eqls('You are not logged in.'); }); it('should return not found if article does not exist', async () => { @@ -231,7 +231,7 @@ describe('API endpoint /articles/', () => { const response = await chai .request(app) .delete('/api/v1/articles/how-to-google-in-2019') - .set('authorization', `Bearer ${userToken}` ); + .set('authorization', `Bearer ${userToken}`); expect(response.status).to.eqls(200); expect(response.body.status).to.eqls('success'); @@ -240,12 +240,12 @@ describe('API endpoint /articles/', () => { it('It should not allow unauthenticated users to delete article', async () => { const response = await chai - .request(app) - .delete('/api/v1/articles/What-a-mighty-God'); + .request(app) + .delete('/api/v1/articles/What-a-mighty-God'); expect(response.status).to.eqls(401); expect(response.body.status).to.eqls('failure'); - expect(response.body.data.message).to.eqls('authentication error'); + expect(response.body.data.message).to.eqls('You are not logged in.'); }); it('should return not found if article does not exist', async () => { diff --git a/server/test/controllers/follow.spec.js b/server/test/controllers/follow.spec.js new file mode 100644 index 00000000..16c75251 --- /dev/null +++ b/server/test/controllers/follow.spec.js @@ -0,0 +1,292 @@ +import chai, { expect } from 'chai'; +import chaiHttp from 'chai-http'; +import sinon from 'sinon'; +import app from '../../..'; +import db from '../../models'; +import TokenManager from '../../helpers/TokenManager'; +import { userToken, userToken2, invalidToken } from '../mockData/tokens' + +const { User, Follow } = db; +// let user1 = TokenManager.sign({ +// id: '45745c60-7b1a-11e8-9c9c-2d42b21b1a3e', +// fullName: 'Jesse', +// userName: 'jesseinit', +// email: 'jesseinit@now.com', +// bio: 'Gitting Started', +// authTypeId: '15745c60-7b1a-11e8-9c9c-2d42b21b1a3e' +// }); + +// let user2 = TokenManager.sign({ +// id: 'aba396bd-7ac4-42c3-b442-cf10dd73e4f4', +// fullName: 'Kabir Alausa', +// userName: 'kabir', +// email: 'kabir@now.com', +// bio: 'Learning life now', +// password: 'Blahblah', +// authTypeId: '15745c60-7b1a-11e8-9c9c-2d42b21b1a3e' +// }); +console.log(userToken) +describe('Follow Model', () => { + + describe('User follows', () => { + it('Should get show proper error message when user is not authorized', async () => { + const response = await chai + .request(app) + .post('/api/v1/users/jesseinit/follow') + .send({}); + + expect(response.status).to.eqls(401); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls( + 'You are not logged in.' + ); + expect(response.body.data.statusCode).to.eqls(401); + }); + + it('Should get show proper error message when user to be followed is not found', async () => { + const response = await chai + .request(app) + .post('/api/v1/users/jesseinitjesseinit/follow') + .set('Authorization', `Bearer ${userToken2}`) + .send({}); + + expect(response.status).to.eqls(404); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls('User not found'); + expect(response.body.data.statusCode).to.eqls(404); + }); + + it('Should get show proper error message when user tries to follow himself/herself', async () => { + const response = await chai + .request(app) + .post('/api/v1/users/jesseinit/follow') + .set('Authorization', `Bearer ${userToken}`) + .send({}); + + expect(response.status).to.eqls(400); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls('You cannot follow yourself'); + expect(response.body.data.statusCode).to.eqls(400); + }); + + it('Should get show proper message when user successfully follows another user', async () => { + const response = await chai + .request(app) + .post('/api/v1/users/jesseinit/follow') + .set('Authorization', `Bearer ${userToken2}`); + + expect(response.status).to.eqls(201); + expect(response.body.status).to.eqls('success'); + expect(response.body.data.statusCode).to.eqls(201); + }); + + it('Should get show proper message when user tries to follow a user he is already following', async () => { + const response = await chai + .request(app) + .post('/api/v1/users/jesseinit/follow') + .set('Authorization', `Bearer ${userToken2}`); + + expect(response.status).to.eqls(400); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.statusCode).to.eqls(400); + }); + + it('Should get handle unexpected errors that occur when trying to follow user', async () => { + const stub = sinon + .stub(Follow, 'findOrCreate') + .callsFake(() => Promise.reject(new Error('Internal Server Error'))); + const response = await chai + .request(app) + .post('/api/v1/users/jesseinit/follow') + .set('Authorization', `Bearer ${userToken2}`) + .send({}); + + expect(response.status).to.eqls(500); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.statusCode).to.eqls(500); + stub.restore(); + }); + }); + + describe('Get followers', () => { + it("Should get show proper error message when getting the followers of users that don't exist", async () => { + const response = await chai + .request(app) + .get('/api/v1/users/jesseinitjesseinit/followers'); + + expect(response.status).to.eqls(404); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls('User not found'); + expect(response.body.data.statusCode).to.eqls(404); + }); + + it('Should get show proper message when getting the followers of users that have followers', async () => { + const response = await chai + .request(app) + .get('/api/v1/users/jesseinit/followers'); + + expect(response.status).to.eqls(200); + expect(response.body.status).to.eqls('success'); + expect(response.body.data.payload.followers).to.be.an('array'); + expect(response.body.data.statusCode).to.eqls(200); + }); + + it('Should get show proper message when user does not have any followers', async () => { + const response = await chai + .request(app) + .get('/api/v1/users/kabir/followers'); + + expect(response.status).to.eqls(200); + expect(response.body.status).to.eqls('success'); + expect(response.body.data.message).to.eqls( + 'You currenly have no followers' + ); + expect(response.body.data.statusCode).to.eqls(200); + }); + + it('Should get handle unexpected errors when trying to get the followers of a user', async () => { + const stub = sinon + .stub(Follow, 'findAll') + .callsFake(() => Promise.reject(new Error('Internal Server Error'))); + const response = await chai + .request(app) + .get('/api/v1/users/kabir/followers'); + + expect(response.status).to.eqls(500); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.statusCode).to.eqls(500); + stub.restore(); + }); + }); + + describe('Get following', () => { + it("Should get show proper error message when getting the following of users that don't exist", async () => { + const response = await chai + .request(app) + .get('/api/v1/users/jesseinitjesseinit/following'); + + expect(response.status).to.eqls(404); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls('User not found'); + expect(response.body.data.statusCode).to.eqls(404); + }); + + it('Should get show proper message when getting the followers of users that have following', async () => { + const response = await chai + .request(app) + .get('/api/v1/users/kabir/following'); + + expect(response.status).to.eqls(200); + expect(response.body.status).to.eqls('success'); + expect(response.body.data.payload.following).to.be.an('array'); + expect(response.body.data.statusCode).to.eqls(200); + }); + + it('Should get show proper message when user is not following any other user', async () => { + const response = await chai + .request(app) + .get('/api/v1/users/jesseinit/following'); + + expect(response.status).to.eqls(200); + expect(response.body.status).to.eqls('success'); + expect(response.body.data.message).to.eqls( + 'You are not following anyone' + ); + expect(response.body.data.statusCode).to.eqls(200); + }); + + it('Should get handle unexpected errors when trying to get the following of a user', async () => { + const stub = sinon + .stub(Follow, 'findAll') + .callsFake(() => Promise.reject(new Error('Internal Server Error'))); + const response = await chai + .request(app) + .get('/api/v1/users/kabir/following'); + + expect(response.status).to.eqls(500); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.statusCode).to.eqls(500); + stub.restore(); + }); + }); + + describe('User Unfollos', () => { + it('Should get show proper error message when user is not authorized to unfollow', async () => { + const response = await chai + .request(app) + .delete('/api/v1/users/jesseinit/unfollow') + .send({}); + + expect(response.status).to.eqls(401); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls( + 'You are not logged in.' + ); + expect(response.body.data.statusCode).to.eqls(401); + }); + + it("Should get show proper error message when trying to unfollow a user doesn't exist", async () => { + const response = await chai + .request(app) + .delete('/api/v1/users/jesseinitjesseinit/unfollow') + .set('Authorization', `Bearer ${userToken2}`); + + expect(response.status).to.eqls(404); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls('User not found'); + expect(response.body.data.statusCode).to.eqls(404); + }); + + it('Should get show proper error message when user tries to unfollow himself/herself', async () => { + const response = await chai + .request(app) + .delete('/api/v1/users/jesseinit/unfollow') + .set('Authorization', `Bearer ${userToken}`); + + expect(response.status).to.eqls(400); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.message).to.eqls( + 'You cannot unfollow yourself' + ); + expect(response.body.data.statusCode).to.eqls(400); + }); + + it('Should get show proper message when user successfully unfollows another user', async () => { + const response = await chai + .request(app) + .delete('/api/v1/users/jesseinit/unfollow') + .set('Authorization', `Bearer ${userToken2}`); + + expect(response.status).to.eqls(200); + expect(response.body.status).to.eqls('success'); + expect(response.body.data.statusCode).to.eqls(200); + }); + + it('Should get show proper message when user tries to unfollow a user he is not following', async () => { + const response = await chai + .request(app) + .delete('/api/v1/users/jesseinit/unfollow') + .set('Authorization', `Bearer ${userToken2}`); + + expect(response.status).to.eqls(400); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.statusCode).to.eqls(400); + }); + + it('Should get handle unexpected errors that occur when trying to unfollow user', async () => { + const stub = sinon + .stub(User, 'findOne') + .callsFake(() => Promise.reject(new Error('Internal Server Error'))); + const response = await chai + .request(app) + .post('/api/v1/users/jesseinit/follow') + .set('Authorization', `Bearer ${userToken2}`) + .send({}); + + expect(response.status).to.eqls(500); + expect(response.body.status).to.eqls('failure'); + expect(response.body.data.statusCode).to.eqls(500); + stub.restore(); + }); + }); +}); diff --git a/server/test/controllers/userController.spec.js b/server/test/controllers/userController.spec.js index 8ef94eed..9da94fe3 100755 --- a/server/test/controllers/userController.spec.js +++ b/server/test/controllers/userController.spec.js @@ -4,6 +4,8 @@ import sinon from 'sinon'; import app from '../../..'; import db from '../../models'; import TokenManager from '../../helpers/TokenManager'; +import token from '../mockData/tokens'; +import { exists } from 'fs'; const { User } = db; @@ -12,10 +14,6 @@ chai.use(chaiHttp); describe('User Model', () => { describe('Password Forget/Reset', () => { let generatedToken = null; - after(async () => { - await User.destroy({ where: {} }); - }); - it('User should get an error when provided email account is not found during password recovery', async () => { const response = await chai .request(app) @@ -39,6 +37,7 @@ describe('User Model', () => { expect(response.status).to.equal(200); expect(response.body.status).to.eqls('success'); expect(response.body.data.message).to.eqls('Kindly check your mail to reset your password'); + }); it('It should be able to handle unexpected DB errors thrown when sending reset link', async () => { @@ -49,9 +48,8 @@ describe('User Model', () => { const response = await chai .request(app) .post('/api/v1/password/forgot') - .send({ - email: 'jesseinit@now.com' - }); + .send({ email: 'jesseinit@now.com' }); + expect(response.status).to.equal(500); stub.restore(); }); @@ -112,6 +110,7 @@ describe('User Model', () => { email: 'jesseinit@now.com' }); + const response = await chai .request(app) .post(`/api/v1/password/reset/${generatedToken}`) diff --git a/server/test/middlewares/middleware.spec.js b/server/test/middlewares/middleware.spec.js new file mode 100644 index 00000000..189e7e8f --- /dev/null +++ b/server/test/middlewares/middleware.spec.js @@ -0,0 +1,25 @@ +import sinon from 'sinon'; +import chai, { expect } from 'chai'; +import sinonChai from 'sinon-chai'; +import AuthMiddleware from '../../middlewares/AuthMiddleware'; +import TokenManager from '../../helpers/TokenManager'; +import db from '../../models'; + +chai.use(sinonChai); + +describe('Middlewares function test', () => { + describe('AuthMiddleware Test', () => { + it('it should throw a 500 server error', async () => { + const req = {}; + const res = { + status() {}, + json() {} + }; + sinon.stub(res, 'status').returnsThis(); + sinon.stub(TokenManager, 'verify').throws(); + AuthMiddleware.checkIfUserIsAuthenticated(req, res, null); + expect(res.status).to.have.been.calledWith(500); + sinon.restore(); + }); + }); +});