Commit 33705cd0 authored by Thennakoon T.M.K.H.B. IT18004564's avatar Thennakoon T.M.K.H.B. IT18004564

Merge branch 'it18004564' into 'master'

add probexpert backend

See merge request !3
parents c70ae3b3 71ee3d1f
node_modules
.env
\ No newline at end of file
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true
}
\ No newline at end of file
require('dotenv').config();
const express = require('express');
const morgan = require('morgan');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(morgan('dev'));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
require('./routes.js')(app);
module.exports = app;
\ No newline at end of file
module.exports = {
port: process.env.PORT,
db: {
prod: process.env.DATABASE_URL_PROD,
test: process.env.DATABASE_URL_DEV,
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
useCreateIndex: true
}
},
jwt: {
secret: process.env.JWT_SECRET,
expiry: '7d'
}
};
const { body, validationResult } = require('express-validator');
exports.loadAnswers = async (req, res, next, id) => {
try {
const answer = await req.question.answers.id(id);
if (!answer) return res.status(404).json({ message: 'Answer not found.' });
req.answer = answer;
} catch (error) {
if (error.name === 'CastError') return res.status(400).json({ message: 'Invalid answer id.' });
return next(error);
}
next();
};
exports.createAnswer = async (req, res, next) => {
const result = validationResult(req);
if (!result.isEmpty()) {
const errors = result.array({ onlyFirstError: true });
return res.status(422).json({ errors });
}
try {
const { id } = req.user;
const { text } = req.body;
const question = await req.question.addAnswer(id, text);
res.status(201).json(question);
} catch (error) {
next(error);
}
};
exports.removeAnswer = async (req, res, next) => {
try {
const { answer } = req.params;
const question = await req.question.removeAnswer(answer);
res.json(question);
} catch (error) {
next(error);
}
};
exports.answerValidate = [
body('text')
.exists()
.trim()
.withMessage('is required')
.notEmpty()
.withMessage('cannot be blank')
.isLength({ min: 30 })
.withMessage('must be at least 30 characters long')
.isLength({ max: 30000 })
.withMessage('must be at most 30000 characters long')
];
const { body, validationResult } = require('express-validator');
exports.loadComments = async (req, res, next, id) => {
try {
let comment;
if (req.answer) {
comment = await req.answer.comments.id(id);
} else {
comment = await req.question.comments.id(id);
}
if (!comment) return res.status(404).json({ message: 'Comment not found.' });
req.comment = comment;
} catch (error) {
if (error.name === 'CastError') return res.status(400).json({ message: 'Invalid comment id.' });
return next(error);
}
next();
};
exports.createComment = async (req, res, next) => {
const result = validationResult(req);
if (!result.isEmpty()) {
const errors = result.array({ onlyFirstError: true });
return res.status(422).json({ errors });
}
try {
const { id } = req.user;
const { comment } = req.body;
if (req.params.answer) {
req.answer.addComment(id, comment);
const question = await req.question.save();
return res.status(201).json(question);
}
const question = await req.question.addComment(id, comment);
return res.status(201).json(question);
} catch (error) {
next(error);
}
};
exports.removeComment = async (req, res, next) => {
const { comment } = req.params;
try {
if (req.params.answer) {
req.answer.removeComment(comment);
const question = await req.question.save();
return res.json(question);
}
const question = await req.question.removeComment(comment);
return res.json(question);
} catch (error) {
next(error);
}
};
exports.validate = [
body('comment')
.exists()
.trim()
.withMessage('is required')
.notEmpty()
.withMessage('cannot be blank')
.isLength({ max: 1000 })
.withMessage('must be at most 1000 characters long')
];
const Question = require('../models/question');
const User = require('../models/user');
const { body, validationResult } = require('express-validator');
exports.loadQuestions = async (req, res, next, id) => {
try {
const question = await Question.findById(id);
if (!question) return res.status(404).json({ message: 'Question not found.' });
req.question = question;
} catch (error) {
if (error.name === 'CastError')
return res.status(400).json({ message: 'Invalid question id.' });
return next(error);
}
next();
};
exports.createQuestion = async (req, res, next) => {
const result = validationResult(req);
if (!result.isEmpty()) {
const errors = result.array({ onlyFirstError: true });
return res.status(422).json({ errors });
}
try {
const { title, tags, text } = req.body;
const author = req.user.id;
const question = await Question.create({
title,
author,
tags,
text
});
res.status(201).json(question);
} catch (error) {
next(error);
}
};
exports.show = async (req, res, next) => {
try {
const { id } = req.question;
const question = await Question.findByIdAndUpdate(
id,
{ $inc: { views: 1 } },
{ new: true }
).populate('answers');
res.json(question);
} catch (error) {
next(error);
}
};
exports.listQuestions = async (req, res, next) => {
try {
const { sortType = '-score' } = req.body;
const questions = await Question.find().sort(sortType);
res.json(questions);
} catch (error) {
next(error);
}
};
exports.listByTags = async (req, res, next) => {
try {
const { sortType = '-score', tags } = req.params;
const questions = await Question.find({ tags: { $all: tags } }).sort(sortType);
res.json(questions);
} catch (error) {
next(error);
}
};
exports.listByUser = async (req, res, next) => {
try {
const { username } = req.params;
const { sortType = '-created' } = req.body;
const author = await User.findOne({ username });
const questions = await Question.find({ author: author.id }).sort(sortType).limit(10);
res.json(questions);
} catch (error) {
next(error);
}
};
exports.removeQuestion = async (req, res, next) => {
try {
await req.question.remove();
res.json({ message: 'Your question successfully deleted.' });
} catch (error) {
next(error);
}
};
exports.loadComment = async (req, res, next, id) => {
try {
const comment = await req.question.comments.id(id);
if (!comment) return res.status(404).json({ message: 'Comment not found.' });
req.comment = comment;
} catch (error) {
if (error.name === 'CastError') return res.status(400).json({ message: 'Invalid comment id.' });
return next(error);
}
next();
};
exports.questionValidate = [
body('title')
.exists()
.trim()
.withMessage('is required')
.notEmpty()
.withMessage('cannot be blank')
.isLength({ max: 180 })
.withMessage('must be at most 180 characters long'),
body('text')
.exists()
.trim()
.withMessage('is required')
.isLength({ min: 10 })
.withMessage('must be at least 10 characters long')
.isLength({ max: 5000 })
.withMessage('must be at most 5000 characters long'),
body('tags').exists().withMessage('is required')
];
const Question = require('../models/question');
exports.listPopulerTags = async (req, res, next) => {
try {
const tags = await Question.aggregate([
{ $project: { tags: 1 } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 25 }
]);
res.json(tags);
} catch (error) {
next(error);
}
};
exports.listTags = async (req, res, next) => {
try {
const tags = await Question.aggregate([
{ $project: { tags: 1 } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
res.json(tags);
} catch (error) {
next(error);
}
};
exports.searchTags = async (req, res, next) => {
const { tag = '' } = req.params;
try {
const tags = await Question.aggregate([
{ $project: { tags: 1 } },
{ $unwind: '$tags' },
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $match: { _id: { $regex: tag, $options: 'i' } } },
{ $sort: { count: -1 } }
]);
res.json(tags);
} catch (error) {
next(error);
}
};
const User = require('../models/user');
const jwtDecode = require('jwt-decode');
const { body, validationResult } = require('express-validator');
const { createToken, hashPassword, verifyPassword } = require('../utils/authentication');
exports.signup = async (req, res) => {
const result = validationResult(req);
if (!result.isEmpty()) {
const errors = result.array({ onlyFirstError: true });
return res.status(422).json({ errors });
}
try {
const { username } = req.body;
const hashedPassword = await hashPassword(req.body.password);
const userData = {
username: username.toLowerCase(),
password: hashedPassword
};
const existingUsername = await User.findOne({
username: userData.username
});
if (existingUsername) {
return res.status(400).json({
message: 'Username already exists.'
});
}
const newUser = new User(userData);
const savedUser = await newUser.save();
if (savedUser) {
const token = createToken(savedUser);
const decodedToken = jwtDecode(token);
const expiresAt = decodedToken.exp;
const { username, role, id, created, profilePhoto } = savedUser;
const userInfo = {
username,
role,
id,
created,
profilePhoto
};
return res.json({
message: 'User created!',
token,
userInfo,
expiresAt
});
} else {
return res.status(400).json({
message: 'There was a problem creating your account.'
});
}
} catch (error) {
return res.status(400).json({
message: 'There was a problem creating your account.'
});
}
};
exports.authenticate = async (req, res) => {
const result = validationResult(req);
if (!result.isEmpty()) {
const errors = result.array({ onlyFirstError: true });
return res.status(422).json({ errors });
}
try {
const { username, password } = req.body;
const user = await User.findOne({
username: username.toLowerCase()
});
if (!user) {
return res.status(403).json({
message: 'Wrong username or password.'
});
}
const passwordValid = await verifyPassword(password, user.password);
if (passwordValid) {
const token = createToken(user);
const decodedToken = jwtDecode(token);
const expiresAt = decodedToken.exp;
const { username, role, id, created, profilePhoto } = user;
const userInfo = { username, role, id, created, profilePhoto };
res.json({
message: 'Authentication successful!',
token,
userInfo,
expiresAt
});
} else {
res.status(403).json({
message: 'Wrong username or password.'
});
}
} catch (error) {
return res.status(400).json({
message: 'Something went wrong.'
});
}
};
exports.listUsers = async (req, res, next) => {
try {
const { sortType = '-created' } = req.body;
const users = await User.find().sort(sortType);
res.json(users);
} catch (error) {
next(error);
}
};
exports.search = async (req, res, next) => {
try {
const users = await User.find({ username: { $regex: req.params.search, $options: 'i' } });
res.json(users);
} catch (error) {
next(error);
}
};
exports.find = async (req, res, next) => {
try {
const users = await User.findOne({ username: req.params.username });
res.json(users);
} catch (error) {
next(error);
}
};
exports.validateUser = [
body('username')
.exists()
.trim()
.withMessage('is required')
.notEmpty()
.withMessage('cannot be blank')
.isLength({ max: 16 })
.withMessage('must be at most 16 characters long')
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage('contains invalid characters'),
body('password')
.exists()
.trim()
.withMessage('is required')
.notEmpty()
.withMessage('cannot be blank')
.isLength({ min: 6 })
.withMessage('must be at least 6 characters long')
.isLength({ max: 50 })
.withMessage('must be at most 50 characters long')
];
exports.upvote = async (req, res) => {
const { id } = req.user;
if (req.answer) {
req.answer.vote(id, 1);
const question = await req.question.save();
return res.json(question);
}
const question = await req.question.vote(id, 1);
return res.json(question);
};
exports.downvote = async (req, res) => {
const { id } = req.user;
if (req.answer) {
req.answer.vote(id, -1);
const question = await req.question.save();
return res.json(question);
}
const question = await req.question.vote(id, -1);
return res.json(question);
};
exports.unvote = async (req, res) => {
const { id } = req.user;
if (req.answer) {
req.answer.vote(id, 0);
const question = await req.question.save();
return res.json(question);
}
const question = await req.question.vote(id, 0);
return res.json(question);
};
const app = require('./app');
const mongoose = require('mongoose');
const config = require('./config');
const connect = (url) => {
return mongoose.connect(url, config.db.options);
};
if (process.env.NODE_ENV === 'production') {
connect(config.db.prod);
app.listen(config.port);
mongoose.connection.on('error', console.log);
} else {
connect(config.db.test);
app.listen(config.port, () =>
console.log(`Runnig on the dev environment - PORT: ${config.port}`)
);
mongoose.connection.on('error', console.log);
}
module.exports = { connect };
module.exports = {
setupFilesAfterEnv: ['./test/setup.js'],
testEnvironment: 'node'
};
\ No newline at end of file
const answerAuth = (req, res, next) => {
if (req.answer.author._id.equals(req.user.id) || req.user.role === 'admin') return next();
res.status(401).end();
};
module.exports = answerAuth;
const commentAuth = (req, res, next) => {
if (req.comment.author._id.equals(req.user.id) || req.user.role === 'admin') return next();
res.status(401).end();
};
module.exports = commentAuth;
const questionsAuth = (req, res, next) => {
if (req.question.author._id.equals(req.user.id) || req.user.role === 'admin') return next();
res.status(401).end();
};
module.exports = questionsAuth;
const jwt = require('jsonwebtoken');
const config = require('../config');
const requireAuth = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: 'Authentication invalid.' });
}
try {
const decodedToken = jwt.verify(token.slice(7), config.jwt.secret, {
algorithm: 'HS256',
expiresIn: config.jwt.expiry
});
req.user = decodedToken;
next();
} catch (error) {
return res.status(401).json({
message: error.message
});
}
};
module.exports = requireAuth;
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const voteSchema = require('./vote');
const commentSchema = require('./comment');
const answerSchema = new Schema({
author: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
created: { type: Date, default: Date.now },
text: { type: String, required: true },
score: { type: Number, default: 0 },
votes: [voteSchema],
comments: [commentSchema]
});
answerSchema.set('toJSON', { getters: true });
answerSchema.methods = {
vote: function (user, vote) {
const existingVote = this.votes.find((v) => v.user._id.equals(user));
if (existingVote) {
// reset score
this.score -= existingVote.vote;
if (vote == 0) {
// remove vote
this.votes.pull(existingVote);
} else {
//change vote
this.score += vote;
existingVote.vote = vote;
}
} else if (vote !== 0) {
// new vote
this.score += vote;
this.votes.push({ user, vote });
}
return this;
},
addComment: function (author, body) {
this.comments.push({ author, body });
return this;
},
removeComment: function (id) {
const comment = this.comments.id(id);
if (!comment) throw new Error('Comment not found');
comment.remove();
return this;
}
};
module.exports = answerSchema;
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const commentSchema = new Schema({
author: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
body: { type: String, required: true },
created: { type: Date, default: Date.now }
});
commentSchema.set('toJSON', { getters: true });
commentSchema.options.toJSON.transform = (doc, ret) => {
const obj = { ...ret };
delete obj._id;
return obj;
};
module.exports = commentSchema;
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const voteSchema = require('./vote');
const commentSchema = require('./comment');
const answerSchema = require('./answer');
const questionSchema = new Schema({
author: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
title: { type: String, required: true },
text: { type: String, required: true },
tags: [{ type: String, required: true }],
score: { type: Number, default: 0 },
votes: [voteSchema],
comments: [commentSchema],
answers: [answerSchema],
created: { type: Date, default: Date.now },
views: { type: Number, default: 0 }
});
questionSchema.set('toJSON', { getters: true });
questionSchema.options.toJSON.transform = (doc, ret) => {
const obj = { ...ret };
delete obj._id;
delete obj.__v;
return obj;
};
questionSchema.methods = {
vote: function (user, vote) {
const existingVote = this.votes.find((v) => v.user._id.equals(user));
if (existingVote) {
// reset score
this.score -= existingVote.vote;
if (vote == 0) {
// remove vote
this.votes.pull(existingVote);
} else {
//change vote
this.score += vote;
existingVote.vote = vote;
}
} else if (vote !== 0) {
// new vote
this.score += vote;
this.votes.push({ user, vote });
}
return this.save();
},
addComment: function (author, body) {
this.comments.push({ author, body });
return this.save();
},
removeComment: function (id) {
const comment = this.comments.id(id);
if (!comment) throw new Error('Comment not found');
comment.remove();
return this.save();
},
addAnswer: function (author, text) {
this.answers.push({ author, text });
return this.save();
},
removeAnswer: function (id) {
const answer = this.answers.id(id);
if (!answer) throw new Error('Answer not found');
answer.remove();
return this.save();
}
};
questionSchema.pre(/^find/, function () {
this.populate('author')
.populate('comments.author', '-role')
.populate('answers.author', '-role')
.populate('answers.comments.author', '-role');
});
questionSchema.pre('save', function (next) {
this.wasNew = this.isNew;
next();
});
questionSchema.post('save', function (doc, next) {
if (this.wasNew) this.vote(this.author._id, 1);
doc
.populate('author')
.populate('answers.author', '-role')
.populate('comments.author', '-role')
.populate('answers.comments.author', '-role')
.execPopulate()
.then(() => next());
});
module.exports = mongoose.model('Question', questionSchema);
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userModel = new Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, required: true, default: 'user' },
profilePhoto: {
type: String,
default: function () {
return `https://secure.gravatar.com/ad/${this._id}?s=90&d=monsterid`;
}
},
created: { type: Date, default: Date.now }
});
userModel.set('toJSON', { getters: true });
userModel.options.toJSON.transform = (doc, ret) => {
const obj = { ...ret };
delete obj._id;
delete obj.__v;
delete obj.password;
return obj;
};
module.exports = mongoose.model('user', userModel);
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const voteSchema = new Schema(
{
user: { type: Schema.Types.ObjectId, required: true },
vote: { type: Number, required: true }
},
{ _id: false }
);
module.exports = voteSchema
\ No newline at end of file
This diff is collapsed.
{
"name": "stackoverflow-api",
"version": "1.0.0",
"description": "Stackoverflow Clone Server",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest"
},
"keywords": [
"Stackoverflow",
"Clone",
"Rest",
"MERN"
],
"author": "Salih Ozdemir",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-validator": "^6.5.0",
"jsonwebtoken": "^8.5.1",
"jwt-decode": "^2.2.0",
"mongoose": "^5.9.16",
"morgan": "^1.10.0"
},
"devDependencies": {
"faker": "^4.1.0",
"jest": "^26.0.1",
"nodemon": "^2.0.4",
"prettier": "^2.0.5",
"supertest": "^4.0.2"
}
}
const {
validateUser,
signup,
authenticate,
listUsers,
search,
find
} = require('./controllers/users');
const {
loadQuestions,
questionValidate,
createQuestion,
show,
listQuestions,
listByTags,
listByUser,
removeQuestion
} = require('./controllers/questions');
const {
loadAnswers,
answerValidate,
createAnswer,
removeAnswer
} = require('./controllers/answers');
const { listPopulerTags, searchTags, listTags } = require('./controllers/tags');
const { upvote, downvote, unvote } = require('./controllers/votes');
const { loadComments, validate, createComment, removeComment } = require('./controllers/comments');
const requireAuth = require('./middlewares/requireAuth');
const questionAuth = require('./middlewares/questionAuth');
const commentAuth = require('./middlewares/commentAuth');
const answerAuth = require('./middlewares/answerAuth');
const router = require('express').Router();
//authentication
router.post('/signup', validateUser, signup);
router.post('/authenticate', validateUser, authenticate);
//users
router.get('/users', listUsers);
router.get('/users/:search', search);
router.get('/user/:username', find);
//questions
router.param('question', loadQuestions);
router.post('/questions', [requireAuth, questionValidate], createQuestion);
router.get('/question/:question', show);
router.get('/question', listQuestions);
router.get('/questions/:tags', listByTags);
router.get('/question/user/:username', listByUser);
router.delete('/question/:question', [requireAuth, questionAuth], removeQuestion);
//tags
router.get('/tags/populertags', listPopulerTags);
router.get('/tags/:tag', searchTags);
router.get('/tags', listTags);
//answers
router.param('answer', loadAnswers);
router.post('/answer/:question', [requireAuth, answerValidate], createAnswer);
router.delete('/answer/:question/:answer', [requireAuth, answerAuth], removeAnswer);
//votes
router.get('/votes/upvote/:question/:answer?', requireAuth, upvote);
router.get('/votes/downvote/:question/:answer?', requireAuth, downvote);
router.get('/votes/unvote/:question/:answer?', requireAuth, unvote);
//comments
router.param('comment', loadComments);
router.post('/comment/:question/:answer?', [requireAuth, validate], createComment);
router.delete('/comment/:question/:comment', [requireAuth, commentAuth], removeComment);
router.delete('/comment/:question/:answer/:comment', [requireAuth, commentAuth], removeComment);
module.exports = (app) => {
app.use('/api', router);
app.use((req, res, next) => {
const error = new Error('Not found');
error.status = 404;
next(error);
});
app.use((error, req, res, next) => {
res.status(error.status || 500).json({
message: error.message
});
});
};
const request = require('supertest');
const mongoose = require('mongoose');
const jwtDecode = require('jwt-decode');
const app = require('../app');
const { validUser } = require('./factories');
const User = mongoose.model('user');
const { hashPassword } = require('../utils/authentication');
process.env.TEST_SUITE = 'auth';
describe('auth endpoints', () => {
let user;
const username = {
nonExisting: 'new',
invalid: 'user!$@',
long: 'a'.repeat(33),
blank: ''
};
const password = {
wrong: 'incorrect',
short: 'aaa',
long: 'a'.repeat(73),
blank: ''
};
beforeEach(async () => {
user = validUser();
const hashedPassword = await hashPassword(user.password);
await new User({ ...user, password: hashedPassword }).save();
});
describe('/authenticate', () => {
test('rejects requests with no credentials', (done) => {
request(app)
.post('/api/authenticate')
.expect((res) => {
expect(res.body.errors).toBeDefined();
res.body.errors.forEach((err) => {
expect(err.msg).toContain('required');
});
})
.expect(422, done);
});
test('reject requests with incorrect name', (done) => {
request(app)
.post('/api/authenticate')
.send({ ...user, username: username.nonExisting })
.expect((res) => {
expect(res.body.message).toContain('Wrong username or password.');
})
.expect(403, done);
});
test('reject requests with incorrect password', (done) => {
request(app)
.post('/api/authenticate')
.send({ ...user, password: password.wrong })
.expect((res) => {
expect(res.body.message).toContain('Wrong username or password.');
})
.expect(403, done);
});
test('rejects requests with invalid name', (done) => {
request(app)
.post('/api/authenticate')
.send({ ...user, username: username.invalid })
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].msg).toContain('invalid');
})
.expect(422, done);
});
test('returns a valid auth token', (done) => {
request(app)
.post('/api/authenticate')
.send(user)
.expect('Content-Type', /json/)
.expect((res) => {
const { token } = res.body;
const decodedToken = jwtDecode(token);
expect(decodedToken.username).toEqual(user.username);
})
.expect(200, done);
});
});
describe('/signup', () => {
test('rejects requests with missing fields', (done) => {
request(app)
.post('/api/signup')
.expect((res) => {
expect(res.body.errors).toBeDefined();
res.body.errors.forEach((err) => {
expect(err.msg).toContain('required');
});
})
.expect(422, done);
});
test('rejects requests with blank name', (done) => {
request(app)
.post('/api/signup')
.send({ ...user, username: username.blank })
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].msg).toContain('cannot be blank');
})
.expect(422, done);
});
test('rejects requests with blank password', (done) => {
request(app)
.post('/api/signup')
.send({ ...user, password: password.blank })
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].msg).toContain('cannot be blank');
})
.expect(422, done);
});
test('rejects requests with invalid name', (done) => {
request(app)
.post('/api/signup')
.send({ ...user, username: username.invalid })
.expect((res) => {
expect(res.body.errors).toBeDefined;
expect(res.body.errors[0].msg).toContain('invalid');
})
.expect(422, done);
});
test('rejects requests with name that is too long', (done) => {
request(app)
.post('/api/signup')
.send({ ...user, username: username.long })
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].msg).toContain(
'at most 32 characters long'
);
})
.expect(422, done);
});
test('rejects request with password that is too long', (done) => {
request(app)
.post('/api/signup')
.send({ ...user, password: password.long })
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].msg).toContain(
'at most 50 characters long'
);
})
.expect(422, done);
});
test('rejects requests with password that is too short', (done) => {
request(app)
.post('/api/signup')
.send({ ...user, password: password.short })
.expect((res) => {
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].msg).toContain(
'at least 6 characters long'
);
})
.expect(422, done);
});
test('rejects requests with existing name', (done) => {
request(app)
.post('/api/signup')
.send(user)
.expect((res) => {
expect(res.body.message).toBeDefined();
expect(res.body.message).toContain('Username already exists.');
})
.expect(400, done);
});
test('creates a new user and returns a valid auth token', (done) => {
request(app)
.post('/api/signup')
.send({ ...user, username: username.nonExisting })
.expect('Content-Type', /json/)
.expect((res) => {
const { token } = res.body;
const decodedToken = jwtDecode(token);
expect(decodedToken.username).toEqual(username.nonExisting);
})
.expect(200, done);
});
});
});
const faker = require('faker');
exports.validUser = () => ({
username: faker.name.firstName().toLowerCase(),
password: 'password'
});
\ No newline at end of file
const mongoose = require('mongoose');
const { connect } = require('../server');
const config = require('../config');
const clearDb = (done) => {
mongoose.connection.dropDatabase();
return done();
};
beforeEach(async (done) => {
if (mongoose.connection.readyState === 0) {
await connect(`${config.db.test}-${process.env.TEST_SUITE}`);
}
return clearDb(done);
});
afterEach(async (done) => {
await mongoose.connection.close();
return done();
});
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const config = require('../config');
const createToken = (user) => {
// Sign the JWT
if (!user.role) {
throw new Error('No user role specified');
}
return jwt.sign(
{
id: user._id,
username: user.username,
role: user.role
},
config.jwt.secret,
{ algorithm: 'HS256', expiresIn: config.jwt.expiry }
);
};
const hashPassword = (password) => {
return new Promise((resolve, reject) => {
// Generate a salt at level 12 strength
bcrypt.genSalt(12, (err, salt) => {
if (err) {
reject(err);
}
bcrypt.hash(password, salt, (err, hash) => {
if (err) {
reject(err);
}
resolve(hash);
});
});
});
};
const verifyPassword = (passwordAttempt, hashedPassword) => {
return bcrypt.compare(passwordAttempt, hashedPassword);
};
module.exports = {
createToken,
hashPassword,
verifyPassword
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment