├── .babelrc ├── .gitignore ├── README.md ├── apidoc.json ├── config ├── env │ ├── development.js │ ├── index.js │ └── test.js ├── express.js └── jwt.js ├── gulpfile.babel.js ├── index.js ├── package.json └── server ├── controllers ├── auth.js ├── tasks.js └── users.js ├── helpers └── clearDb.js ├── models ├── task.js └── user.js ├── routes ├── _apidoc.js ├── auth.js ├── index.js ├── tasks.js ├── users.js └── validation │ └── tasks.js └── test └── routes └── tasks.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-2" 5 | ], 6 | "plugins": [ 7 | "add-module-exports" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | .project 4 | .classpath 5 | .class 6 | .preferences 7 | .idea 8 | dist 9 | node_modules 10 | docs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RESTful API with Node.js 2 | ## Built using Babel, Express and Mongoose 3 | 4 | This repository is the final result of the series of posts [Building a REST API with Node.js](http://blog.mpayetta.com/node.js/2016/07/22/building-a-node-restful-api-intro-and-setup/) 5 | listed in my [blog](http://blog.mpayetta.com). 6 | 7 | Features like validation and unit testing are only implemented for some of the routes since the goal is to just provide 8 | them as an example to develop any kind of RESTful API. 9 | 10 | ## Installation and running 11 | 12 | The project depends on a mongodb instance that must be accessible, you can change the connection details in the 13 | `config/env/development.js` and `config/env/test.js` for the testing database. 14 | 15 | 1. Clone this repository 16 | 2. `cd` into the cloned copy and run `npm install` 17 | 3. Run `gulp nodemon` 18 | 4. Try it in a console or in a browser window doing a `GET` to `http://localhost:3000/api/api-status` 19 | 20 | ## Unit testing 21 | 22 | To run unit tests simply run `gulp mocha`. 23 | 24 | -------------------------------------------------------------------------------- /apidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NodeES6API", 3 | "version": "0.1.0", 4 | "description": "An example API built with Node and ES6", 5 | "title": "Node ES6 API Docs", 6 | "url" : "https://github.com/mpayetta/node-es6-rest-api" 7 | } -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | export default { 2 | env: 'development', 3 | db: 'mongodb://localhost/node-es6-api-dev', 4 | port: 3000, 5 | jwtSecret: 'my-api-secret', 6 | jwtDuration: '2 hours' 7 | }; 8 | -------------------------------------------------------------------------------- /config/env/index.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV || 'development'; 2 | const config = require(`./${env}`); 3 | 4 | export default config; 5 | -------------------------------------------------------------------------------- /config/env/test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | env: 'test', 3 | db: 'mongodb://localhost/node-es6-api-test', 4 | port: 3000, 5 | jwtSecret: 'my-api-secret', 6 | jwtDuration: '2 hours' 7 | }; 8 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import expressValidation from 'express-validation'; 3 | import bodyParser from 'body-parser'; 4 | import routes from '../server/routes'; 5 | 6 | const app = express(); 7 | 8 | app.use(bodyParser.json()); 9 | app.use(bodyParser.urlencoded({ extended: true })); 10 | 11 | // mount all routes on /api path 12 | app.use('/api', routes); 13 | 14 | app.use((err, req, res, next) => { 15 | if (err instanceof expressValidation.ValidationError) { 16 | res.status(err.status).json(err); 17 | } else { 18 | res.status(500) 19 | .json({ 20 | status: err.status, 21 | message: err.message 22 | }); 23 | } 24 | }); 25 | 26 | 27 | export default app; -------------------------------------------------------------------------------- /config/jwt.js: -------------------------------------------------------------------------------- 1 | import config from './env'; 2 | import jwt from 'express-jwt'; 3 | 4 | const authenticate = jwt({ 5 | secret: config.jwtSecret 6 | }); 7 | 8 | export default authenticate; 9 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import loadPlugins from 'gulp-load-plugins'; 3 | import path from 'path'; 4 | import del from 'del'; 5 | import runSequence from 'run-sequence'; 6 | import babelCompiler from 'babel-core/register'; 7 | 8 | // Load the gulp plugins into the `plugins` variable 9 | const plugins = loadPlugins(); 10 | 11 | const paths = { 12 | js: ['./**/*.js', '!dist/**', '!node_modules/**'], 13 | tests: './server/test/**/*.test.js' 14 | }; 15 | 16 | // Compile all Babel Javascript into ES5 and put it into the dist dir 17 | gulp.task('babel', () => { 18 | return gulp.src(paths.js, { base: '.' }) 19 | .pipe(plugins.babel()) 20 | .pipe(gulp.dest('dist')); 21 | }); 22 | 23 | // Start server with restart on file change events 24 | gulp.task('nodemon', ['babel'], () => 25 | plugins.nodemon({ 26 | script: path.join('dist', 'index.js'), 27 | ext: 'js', 28 | ignore: ['node_modules/**/*.js', 'dist/**/*.js'], 29 | tasks: ['babel'] 30 | }) 31 | ); 32 | 33 | // Clean up dist directory 34 | gulp.task('clean', () => { 35 | return del('dist/**'); 36 | }); 37 | 38 | // Set environment variables 39 | gulp.task('set-env', () => { 40 | plugins.env({ 41 | vars: { 42 | NODE_ENV: 'test' 43 | } 44 | }); 45 | }); 46 | 47 | // triggers mocha tests 48 | gulp.task('test', ['set-env'], () => { 49 | let exitCode = 0; 50 | 51 | return gulp.src([paths.tests], { read: false }) 52 | .pipe(plugins.plumber()) 53 | .pipe(plugins.mocha({ 54 | reporter:'spec', 55 | ui: 'bdd', 56 | timeout: 2000, 57 | compilers: { 58 | js: babelCompiler 59 | } 60 | })) 61 | .once('error', (err) => { 62 | console.log(err); 63 | exitCode = 1; 64 | }) 65 | .once('end', () => { 66 | process.exit(exitCode); 67 | }); 68 | }); 69 | 70 | gulp.task('mocha', ['clean'], () => { 71 | return runSequence('babel', 'test'); 72 | }); 73 | 74 | gulp.task('apidoc', (done) => { 75 | plugins.apidoc({ 76 | src: 'server/routes/', 77 | dest: 'docs/', 78 | config: '' 79 | }, done); 80 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import app from './config/express'; 3 | import config from './config/env'; 4 | 5 | mongoose.connect(config.db); 6 | mongoose.connection.on('error', () => { 7 | throw new Error(`unable to connect to database: ${config.db}`); 8 | }); 9 | mongoose.connection.on('connected', () => { 10 | console.log(`Connected to database: ${config.db}`); 11 | }); 12 | 13 | if (config.env === 'development') { 14 | mongoose.set('debug', true); 15 | } 16 | 17 | app.listen(config.port, () => { 18 | console.log(`API Server started and listening on port ${config.port} (${config.env})`); 19 | }); 20 | 21 | export default app; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-es6-api", 3 | "version": "1.0.0", 4 | "description": "A node.js RESTful API built with ES6", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm test" 8 | }, 9 | "author": "Mauricio Payetta (http://www.mauriciopayetta.com)", 10 | "license": "ISC", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mpayetta/node-es6-rest-api.git" 14 | }, 15 | "dependencies": { 16 | "async": "^2.0.1", 17 | "bcrypt": "^0.8.7", 18 | "body-parser": "^1.15.2", 19 | "express": "^4.13.4", 20 | "express-jwt": "^3.4.0", 21 | "express-validation": "^0.4.5", 22 | "joi": "^7.3.0", 23 | "jsonwebtoken": "^7.1.6", 24 | "kerberos": "0.0.21", 25 | "mongoose": "^4.3.7" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.11.4", 29 | "babel-plugin-add-module-exports": "^0.2.1", 30 | "babel-preset-es2015": "^6.5.0", 31 | "babel-preset-stage-2": "^6.5.0", 32 | "chai": "^3.5.0", 33 | "del": "^2.2.1", 34 | "gulp": "^3.9.1", 35 | "gulp-apidoc": "^0.2.4", 36 | "gulp-babel": "^6.1.2", 37 | "gulp-env": "^0.4.0", 38 | "gulp-load-plugins": "^1.2.4", 39 | "gulp-mocha": "^2.2.0", 40 | "gulp-nodemon": "^2.1.0", 41 | "gulp-plumber": "^1.1.0", 42 | "http-status": "^0.2.3", 43 | "mocha": "^2.5.3", 44 | "run-sequence": "^1.2.2", 45 | "sinon": "^1.17.5", 46 | "sinon-as-promised": "^3.0.1", 47 | "sinon-mongoose": "^1.2.1", 48 | "supertest": "^1.2.0", 49 | "supertest-as-promised": "^2.0.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/controllers/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import config from '../../config/env'; 3 | import User from '../models/user'; 4 | 5 | function authenticate(req, res, next) { 6 | User.findOne({ 7 | username: req.body.username 8 | }) 9 | .exec() 10 | .then((user) => { 11 | if (!user) return next(); 12 | user.comparePassword(req.body.password, (e, isMatch) => { 13 | if (e) return next(e); 14 | if (isMatch) { 15 | req.user = user; 16 | next(); 17 | } else { 18 | return next(); 19 | } 20 | }); 21 | }, (e) => next(e)) 22 | } 23 | 24 | function generateToken(req, res, next) { 25 | if (!req.user) return next(); 26 | 27 | const jwtPayload = { 28 | id: req.user._id 29 | }; 30 | const jwtData = { 31 | expiresIn: config.jwtDuration, 32 | }; 33 | const secret = config.jwtSecret; 34 | req.token = jwt.sign(jwtPayload, secret, jwtData); 35 | 36 | next(); 37 | } 38 | 39 | function respondJWT(req, res) { 40 | if (!req.user) { 41 | res.status(401).json({ 42 | error: 'Unauthorized' 43 | }); 44 | } else { 45 | res.status(200).json({ 46 | jwt: req.token 47 | }); 48 | } 49 | } 50 | 51 | export default { authenticate, generateToken, respondJWT }; -------------------------------------------------------------------------------- /server/controllers/tasks.js: -------------------------------------------------------------------------------- 1 | import Task from '../models/task'; 2 | 3 | function load(req, res, next, id) { 4 | Task.findById(id) 5 | .exec() 6 | .then((task) => { 7 | req.dbTask = task; 8 | return next(); 9 | }, (e) => next(e)); 10 | } 11 | 12 | function get(req, res) { 13 | return res.json(req.dbTask); 14 | } 15 | 16 | function create(req, res, next) { 17 | Task.create({ 18 | user: req.body.user, 19 | description: req.body.description 20 | }) 21 | .then((savedTask) => { 22 | return res.json(savedTask); 23 | }, (e) => next(e)); 24 | } 25 | 26 | function update(req, res, next) { 27 | const task = req.dbTask; 28 | Object.assign(task, req.body); 29 | 30 | task.save() 31 | .then(() => res.sendStatus(204), 32 | (e) => next(e)); 33 | } 34 | 35 | function list(req, res, next) { 36 | const { limit = 50, skip = 0 } = req.query; 37 | Task.find() 38 | .skip(skip) 39 | .limit(limit) 40 | .exec() 41 | .then((tasks) => res.json(tasks), 42 | (e) => next(e)); 43 | } 44 | 45 | function remove(req, res, next) { 46 | const task = req.dbTask; 47 | task.remove() 48 | .then(() => res.sendStatus(204), 49 | (e) => next(e)); 50 | } 51 | 52 | export default { load, get, create, update, list, remove }; 53 | -------------------------------------------------------------------------------- /server/controllers/users.js: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | 3 | function load(req, res, next, id) { 4 | User.findById(id) 5 | .exec() 6 | .then((user) => { 7 | if (!user) { 8 | return res.status(404).json({ 9 | status: 400, 10 | message: "User not found" 11 | }); 12 | } 13 | req.dbUser = user; 14 | return next(); 15 | }, (e) => next(e)); 16 | } 17 | 18 | function get(req, res) { 19 | return res.json(req.dbUser); 20 | } 21 | 22 | function create(req, res, next) { 23 | User.create({ 24 | username: req.body.username, 25 | password: req.body.password 26 | }) 27 | .then((savedUser) => { 28 | return res.json(savedUser); 29 | }, (e) => next(e)); 30 | } 31 | 32 | function update(req, res, next) { 33 | const user = req.dbUser; 34 | Object.assign(user, req.body); 35 | 36 | user.save() 37 | .then(() => res.sendStatus(204), 38 | (e) => next(e)); 39 | } 40 | 41 | function list(req, res, next) { 42 | const { limit = 50, skip = 0 } = req.query; 43 | User.find() 44 | .skip(skip) 45 | .limit(limit) 46 | .exec() 47 | .then((users) => res.json(users), 48 | (e) => next(e)); 49 | } 50 | 51 | function remove(req, res, next) { 52 | const user = req.dbUser; 53 | user.remove() 54 | .then(() => res.sendStatus(204), 55 | (e) => next(e)); 56 | } 57 | 58 | export default { load, get, create, update, list, remove }; 59 | -------------------------------------------------------------------------------- /server/helpers/clearDb.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import async from 'async'; 3 | 4 | export function clearDatabase(callback) { 5 | if (process.env.NODE_ENV !== 'test') { 6 | throw new Error('Attempt to clear non testing database!'); 7 | } 8 | 9 | const fns = []; 10 | 11 | function createAsyncFn(index) { 12 | fns.push((done) => { 13 | mongoose.connection.collections[index].remove(() => { 14 | done(); 15 | }); 16 | }); 17 | } 18 | 19 | for (const i in mongoose.connection.collections) { 20 | if (mongoose.connection.collections.hasOwnProperty(i)) { 21 | createAsyncFn(i); 22 | } 23 | } 24 | 25 | async.parallel(fns, () => callback()); 26 | } 27 | -------------------------------------------------------------------------------- /server/models/task.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const TaskSchema = new mongoose.Schema({ 4 | user: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | required: true, 7 | ref: 'users' 8 | }, 9 | description: { 10 | type: String, 11 | required: true, 12 | trim: true 13 | }, 14 | done: { 15 | type: Boolean, 16 | default: false 17 | } 18 | }); 19 | 20 | export default mongoose.model('Task', TaskSchema); 21 | 22 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | const UserSchema = new mongoose.Schema({ 5 | username: { 6 | type: String, 7 | required: true, 8 | trim: true 9 | }, 10 | password: { 11 | type: String, 12 | required: true, 13 | trim: true 14 | } 15 | }); 16 | 17 | UserSchema.pre('save', function (next) { 18 | const user = this; 19 | 20 | if (!user.isModified('password')) { 21 | return next(); 22 | } 23 | 24 | bcrypt.genSalt(10, (err, salt) => { 25 | if (err) return next(err); 26 | 27 | bcrypt.hash(user.password, salt, (hashErr, hash) => { 28 | if (hashErr) return next(hashErr); 29 | 30 | user.password = hash; 31 | next(); 32 | }); 33 | }); 34 | }); 35 | 36 | UserSchema.methods.comparePassword = function (toCompare, done) { 37 | bcrypt.compare(toCompare, this.password, (err, isMatch) => { 38 | if (err) done(err); 39 | else done(err, isMatch); 40 | }); 41 | }; 42 | 43 | export default mongoose.model('User', UserSchema); 44 | -------------------------------------------------------------------------------- /server/routes/_apidoc.js: -------------------------------------------------------------------------------- 1 | // ----------------------- 2 | // Headers 3 | // ----------------------- 4 | 5 | /** 6 | * @apiDefine AuthorizationHeader 7 | * 8 | * @apiHeader {Object} Authorization header value must follow the pattern 9 | * "JWT [token sting]" 10 | * 11 | * @apiHeaderExample {json} Authorization Header Example: 12 | * { 13 | * "Authorization": "JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..." 14 | * } 15 | * 16 | */ 17 | 18 | // ----------------------- 19 | // Error Responses 20 | // ----------------------- 21 | 22 | /** 23 | * @apiDefine NotAuthorizedError 24 | * 25 | * @apiError Unauthorized The JWT is missing or not valid. 26 | * 27 | * @apiErrorExample Unauthorized Response: 28 | * HTTP/1.1 401 Unauthorized 29 | * { 30 | * "status": 401, 31 | * "message": "No authorization token was found" 32 | * } 33 | */ 34 | 35 | /** 36 | * @apiDefine InternalServerError 37 | * 38 | * @apiError InternalError There was an internal error when trying to serve the request 39 | * 40 | * @apiErrorExample InternalError Response: 41 | * HTTP/1.1 500 Internal Server Error 42 | * { 43 | * "status": 500, 44 | * "message": "There was an internal error when trying to serve the request" 45 | * } 46 | */ 47 | 48 | 49 | // ----------------------- 50 | // Success Responses 51 | // ----------------------- 52 | 53 | /** 54 | * @apiDefine NoContentResponse 55 | * @apiDescription Empty successful response 56 | * 57 | * @apiSuccessExample NoContent Response: 58 | * HTTP/1.1 204 No Content 59 | */ -------------------------------------------------------------------------------- /server/routes/auth.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import authCtrl from '../controllers/auth'; 3 | 4 | const router = express.Router(); 5 | 6 | router.route('/token') 7 | /** POST /api/auth/token Get JWT authentication token */ 8 | .post(authCtrl.authenticate, 9 | authCtrl.generateToken, 10 | authCtrl.respondJWT); 11 | 12 | export default router; -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import userRoutes from './users'; 3 | import taskRoutes from './tasks'; 4 | import authRoutes from './auth'; 5 | 6 | const router = express.Router(); // eslint-disable-line new-cap 7 | 8 | /** GET /api-status - Check service status **/ 9 | router.get('/api-status', (req, res) => 10 | res.json({ 11 | status: "ok" 12 | }) 13 | ); 14 | 15 | router.use('/users', userRoutes); 16 | router.use('/tasks', taskRoutes); 17 | router.use('/auth', authRoutes); 18 | 19 | export default router; 20 | -------------------------------------------------------------------------------- /server/routes/tasks.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import validate from 'express-validation'; 3 | import taskCtrl from '../controllers/tasks'; 4 | import validations from './validation/tasks'; 5 | 6 | const router = express.Router(); 7 | 8 | router.route('/') 9 | /** GET /api/tasks - Get list of tasks */ 10 | .get(taskCtrl.list) 11 | 12 | /** POST /api/tasks - Create new task */ 13 | .post(validate(validations.createTask), 14 | taskCtrl.create); 15 | 16 | router.route('/:taskId') 17 | /** GET /api/tasks/:taskId - Get task */ 18 | .get(taskCtrl.get) 19 | 20 | /** PUT /api/tasks/:taskId - Update task */ 21 | .put(validate(validations.updateTask), 22 | taskCtrl.update) 23 | 24 | /** DELETE /api/tasks/:taskId - Delete task */ 25 | .delete(taskCtrl.remove); 26 | 27 | /** Load task when API with taskId route parameter is hit */ 28 | router.param('taskId', validate(validations.getTask)); 29 | router.param('taskId', taskCtrl.load); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /server/routes/users.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import userCtrl from '../controllers/users'; 3 | import auth from '../../config/jwt'; 4 | 5 | const router = express.Router(); 6 | 7 | /** 8 | * @apiDefine UserNotFoundError 9 | * 10 | * @apiError UserNotFound The id of the User was not found. 11 | * 12 | * @apiErrorExample UserNotFound Response: 13 | * HTTP/1.1 404 Not Found 14 | * { 15 | * "status": 400, 16 | * "message": "User not found" 17 | * } 18 | */ 19 | 20 | router.route('/') 21 | /** 22 | * @api {get} /api/users List all users 23 | * @apiName GetUsers 24 | * @apiGroup Users 25 | * 26 | * @apiUse AuthorizationHeader 27 | * 28 | * @apiSuccess {Object[]} users List of users 29 | * @apiSuccess {String} users._id ID of the user 30 | * @apiSuccess {String} users.username Username of the user 31 | * @apiSuccess {String} users.password Encrypted password of the user 32 | * username/password combination. 33 | * 34 | * @apiSuccessExample Success Response: 35 | * HTTP/1.1 200 Success 36 | * [ 37 | * { 38 | * "_id": 39 | * "username": "a_user", 40 | * "password": "$2a$10$4kGSUCjFpSSIGS6T3Vpb7O..." 41 | * }, 42 | * { 43 | * "_id": 44 | * "username": "another_user", 45 | * "password": "$2a$10$4kGSUCjFpSSIGS6T3Vpb7O..." 46 | * } 47 | * ] 48 | * 49 | * @apiUse NotAuthorizedError 50 | * @apiUse InternalServerError 51 | **/ 52 | .get(userCtrl.list) 53 | 54 | /** POST /api/users - Create new user */ 55 | .post(userCtrl.create); 56 | 57 | router.route('/:userId') 58 | /** GET /api/users/:userId - Get user */ 59 | .get(auth, userCtrl.get) 60 | 61 | /** PUT /api/users/:userId - Update user */ 62 | .put(auth, userCtrl.update) 63 | 64 | /** DELETE /api/users/:userId - Delete user */ 65 | .delete(auth, userCtrl.remove); 66 | 67 | /** Load user when API with userId route parameter is hit */ 68 | router.param('userId', userCtrl.load); 69 | 70 | export default router; 71 | -------------------------------------------------------------------------------- /server/routes/validation/tasks.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | 3 | export default { 4 | // POST /api/tasks 5 | createTask: { 6 | body: { 7 | user: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required(), 8 | description: Joi.string().required(), 9 | done: Joi.boolean() 10 | } 11 | }, 12 | 13 | // GET /api/tasks/:taskId 14 | getTask: { 15 | params: { 16 | taskId: Joi.string().regex(/^[0-9a-fA-F]{24}$/).required() 17 | } 18 | }, 19 | 20 | // PUT /api/tasks/:taskId 21 | updateTask: { 22 | body: { 23 | user: Joi.string().regex(/^[0-9a-fA-F]{24}$/), 24 | description: Joi.string(), 25 | done: Joi.boolean() 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /server/test/routes/tasks.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest-as-promised'; 2 | import httpStatus from 'http-status'; 3 | import { expect } from 'chai'; 4 | import sinon from 'sinon'; 5 | import app from '../../../index'; 6 | import { clearDatabase } from '../../helpers/ClearDB'; 7 | import User from '../../models/user'; 8 | import Task from '../../models/task'; 9 | 10 | require('sinon-mongoose'); 11 | require('sinon-as-promised'); 12 | 13 | describe('## Tasks API Tests', () => { 14 | 15 | let sandbox, user; 16 | 17 | before((done) => { 18 | User.create({ 19 | username: 'testuser', 20 | password: 'testuser' 21 | }).then((u) => { 22 | user = u; 23 | done(); 24 | }) 25 | }); 26 | 27 | beforeEach((done) => { 28 | clearDatabase(() => { 29 | sandbox = sinon.sandbox.create(); 30 | done(); 31 | }); 32 | }); 33 | 34 | afterEach((done) => { 35 | sandbox.restore(); 36 | done(); 37 | }); 38 | 39 | describe('### GET /tasks', () => { 40 | 41 | }); 42 | 43 | describe('### GET /tasks/:taskId', () => { 44 | 45 | }); 46 | 47 | describe('### POST /tasks', () => { 48 | it('should return the created task successfully', (done) => { 49 | request(app) 50 | .post('/api/tasks') 51 | .send({ 52 | user: user._id, 53 | description: 'this is a test task' 54 | }) 55 | .expect(httpStatus.OK) 56 | .then(res => { 57 | expect(res.body.user).to.equal(user._id.toString()); 58 | expect(res.body.description).to.equal('this is a test task'); 59 | expect(res.body._id).to.exist; 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should return Internal Server Error when mongoose fails to save task', (done) => { 65 | const createStub = sandbox.stub(Task, 'create'); 66 | createStub.rejects({}); 67 | request(app) 68 | .post('/api/tasks') 69 | .send({ 70 | user: user._id, 71 | description: 'this is a test task' 72 | }) 73 | .expect(httpStatus.INTERNAL_SERVER_ERROR) 74 | .then(() => done()); 75 | }); 76 | 77 | it('should return Bad Request when missing user', (done) => { 78 | request(app) 79 | .post('/api/tasks') 80 | .send({ 81 | description: 'this is a test task' 82 | }) 83 | .expect(httpStatus.BAD_REQUEST) 84 | .then(() => done()); 85 | }); 86 | }); 87 | 88 | describe('### PUT /tasks/:taskId', () => { 89 | 90 | }); 91 | 92 | describe('### DELETE /tasks/:taskId', () => { 93 | 94 | }); 95 | 96 | }); --------------------------------------------------------------------------------