Skip to content
This repository has been archived by the owner on Jul 16, 2022. It is now read-only.

#166473183 Add auth functionality #25

Merged
merged 1 commit into from Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .codeclimate.yml
@@ -1,2 +1,3 @@
exclude_patterns:
- 'src/database'
- '**/*.spec.js'
158 changes: 158 additions & 0 deletions src/app/user/_tests_/user.spec.js
@@ -0,0 +1,158 @@
import chai, { expect } from 'chai';
import chaiHttp from 'chai-http';
import models from '../../../database/models';
import server from '../../../index';

chai.use(chaiHttp);

const baseUrl = '/api/v1/users';

let uniqueUser;
const userRequestObject = {
username: 'buttercup',
email: 'buttercup@puffmail.com',
firstName: 'butter',
password: 'superbuttercup',
};

describe('User Test Suite', () => {
before(async () => {
await models.sequelize.sync({ force: true });
const requestObject = {
...userRequestObject,
...{
username: 'uniqueUser',
email: 'unique@email.com',
},
};
uniqueUser = (await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject)).body.user;
});

describe('Input Validation', () => {
it('should not signup a user with missing required field', async () => {
const requestObject = {
...userRequestObject,
...{ username: undefined },
};
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject);
const errorMessage = 'username is required';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(400);
});

it('should not signup a user with empty param', async () => {
const requestObject = { ...userRequestObject, ...{ username: '' } };
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject);
const errorMessage = 'username must not be empty';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(400);
});

it('should not signup a user with invalid email', async () => {
const requestObject = { ...userRequestObject, ...{ email: 'invalid' } };
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject);
const errorMessage = 'Please enter a valid email';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(400);
});

it('should not signup a user with invalid input type', async () => {
const requestObject = { ...userRequestObject };
requestObject.firstName = ['butter', 'cup'];
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject);
const errorMessage = '[firstName] must be of type: string';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(400);
});

it('should not signup a user if password is less than 8', async () => {
const requestObject = { ...userRequestObject, ...{ password: 'bugs' } };
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject);
const errorMessage = 'Your password must be at least 8 characters';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(400);
});

it('should not signup a user if username is not unique', async () => {
const requestObject = { ...userRequestObject };
requestObject.username = uniqueUser.username;
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject);
const errorMessage = 'username already in use';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(409);
});

it('should not signup a user if email is not unique', async () => {
const requestObject = { ...userRequestObject };
requestObject.email = uniqueUser.email;
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(requestObject);
const errorMessage = 'email already in use';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(409);
});

it('should signup a user and return token if valid inputs', async () => {
const response = await chai
.request(server)
.post(`${baseUrl}/auth/signup`)
.send(userRequestObject);
expect(response.body.message).to.equal('Registration Successful!');
expect(response.body.token.length).to.be.greaterThan(0);
expect(response.status).to.equal(201);
});
});

describe('Login', () => {
it('should not login a user with invalid credentials', async () => {
const requestObject = {
usernameOrEmail: uniqueUser.username,
password: 'invalid-password',
};
const response = await chai
.request(server)
.post(`${baseUrl}/auth/login`)
.send(requestObject);
const errorMessage = 'Invalid credentials';
expect(response.body.error).to.equal(errorMessage);
expect(response.status).to.equal(401);
});

it('should return token if credentials are valid', async () => {
const requestObject = {
usernameOrEmail: uniqueUser.username,
password: userRequestObject.password,
};
const response = await chai
.request(server)
.post(`${baseUrl}/auth/login`)
.send(requestObject);
expect(response.body.message).to.equal('Login Successful!');
expect(response.body.token.length).to.be.greaterThan(0);
expect(response.status).to.equal(201);
});
});
});
19 changes: 8 additions & 11 deletions src/common/helpers/paramValidator.js
Expand Up @@ -7,7 +7,7 @@ const requiredParamsValidator = (body, requiredParams, next) => {
if (!Object.keys(body).includes(param)) {
errorArray.push(`${param} is required`);
}
if (param.trim().length < 1) {
if (Object.keys(body).includes(param) && body[param].length < 1) {
errorArray.push(`${param} must not be empty`);
}
});
Expand Down Expand Up @@ -57,17 +57,14 @@ const typeValidator = (params, type, next) => {
const uniqueParamValidator = async (param, model, next) => {
const paramKey = Object.keys(param);
const paramValue = String(Object.values(param)).toLowerCase();
const paramExists = await models[model].findOne({
where: { [paramKey]: paramValue },
});

if (paramValue) {
const paramExists = await models[model].findOne({
where: { [paramKey]: paramValue },
});
if (paramExists) {
const error = new Error(`${paramKey} already in use`);
error.status = 409;
return next(error);
}
return next();
if (paramExists) {
const error = new Error(`${paramKey} already in use`);
error.status = 409;
return next(error);
}

return next();
Expand Down
2 changes: 2 additions & 0 deletions src/common/index.js
Expand Up @@ -4,3 +4,5 @@ export {
uniqueParamValidator,
generateToken,
} from './helpers';

export { notFoundRoute, errorHandler } from './middleware';
33 changes: 33 additions & 0 deletions src/common/middleware/authentication.js
@@ -0,0 +1,33 @@
import jwt from 'jsonwebtoken';
import models from '../../database/models';

const { User } = models;

const secret = process.env.SECRET_KEY;

const isTokenValid = (req, res, next) => {
const token = req.headers.authorization;
jwt.verify(token, secret, async (err, decoded) => {
let error;
if (!token) {
error = new Error('No token provided');
error.status = 401;
return next(error);
}
if (err) {
error = new Error('Invalid token');
error.status = 401;
return next(error);
}
const user = await User.findByPk(decoded.id);
if (!user) {
error = new Error('Invalid token');
error.status = 401;
return next(error);
}
req.decoded = decoded;
return next();
});
};

export default isTokenValid;
1 change: 1 addition & 0 deletions src/common/middleware/index.js
@@ -1,2 +1,3 @@
export { default as notFoundRoute } from './notFoundRoute';
export { default as errorHandler } from './errorHandler';
export { default as authentication } from './authentication';
35 changes: 16 additions & 19 deletions src/database/models/averageRating.js
@@ -1,26 +1,23 @@
export default (sequelize, DataTypes) => {
const AverageRating = sequelize.define(
'AverageRating',
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
},
resourceId: {
allowNull: false,
type: DataTypes.UUID
},
averageRating: {
allowNull: false,
type: DataTypes.FLOAT,
}
const AverageRating = sequelize.define('AverageRating', {
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
);
resourceId: {
allowNull: false,
type: DataTypes.UUID,
},
averageRating: {
allowNull: false,
type: DataTypes.FLOAT,
},
});

AverageRating.associate = (models) => {
AverageRating.associate = models => {
AverageRating.belongsTo(models.Resource, {
foreignKey: 'averageRating'
foreignKey: 'averageRatingId',
});
};

Expand Down
55 changes: 25 additions & 30 deletions src/database/models/languageStack.js
@@ -1,40 +1,35 @@
export default (sequelize, DataTypes) => {
const LanguageStack = sequelize.define(
'LanguageStack',
{
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
},
userId: {
allowNull: false,
type: DataTypes.UUID
},
stackId: {
allowNull: false,
type: DataTypes.UUID
},
languageId: {
allowNull: false,
type: DataTypes.UUID
},
const LanguageStack = sequelize.define('LanguageStack', {
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
);
userId: {
allowNull: false,
type: DataTypes.UUID,
},
stackId: {
allowNull: false,
type: DataTypes.UUID,
},
languageId: {
allowNull: false,
type: DataTypes.UUID,
},
});

LanguageStack.associate = (models) => {
LanguageStack.belongsTo(models.Language, {
foreignKey: 'LanguageId',
as: 'language'
LanguageStack.associate = models => {
LanguageStack.hasMany(models.Language, {
foreignKey: 'LanguageStackId',
});
LanguageStack.belongsTo(models.Stack, {
foreignKey: 'StackId',
as: 'stack'
LanguageStack.hasMany(models.Stack, {
foreignKey: 'LanguageStackId',
});
LanguageStack.belongsTo(models.User, {
foreignKey: 'userId',
as: 'user'
as: 'user',
});
};

Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Expand Up @@ -4,7 +4,7 @@ import swaggerUi from 'swagger-ui-express';
import cors from 'cors';
import dotenv from 'dotenv';
import userRoutes from './app';
import { notFoundRoute, errorHandler } from './common/middleware';
import { notFoundRoute, errorHandler } from './common';
import { Api } from '../docs';

dotenv.config();
Expand Down