Repository: adrianObel/koa2-api-boilerplate Branch: master Commit: 7c9c26b7f9d6 Files: 27 Total size: 27.8 KB Directory structure: gitextract_sjp2bf63/ ├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── bin/ │ └── server.js ├── config/ │ ├── env/ │ │ ├── common.js │ │ ├── development.js │ │ ├── production.js │ │ └── test.js │ ├── index.js │ └── passport.js ├── index.js ├── package.json ├── src/ │ ├── middleware/ │ │ ├── index.js │ │ └── validators.js │ ├── models/ │ │ └── users.js │ ├── modules/ │ │ ├── auth/ │ │ │ ├── controller.js │ │ │ └── router.js │ │ ├── index.js │ │ └── users/ │ │ ├── controller.js │ │ └── router.js │ └── utils/ │ └── auth.js └── test/ ├── auth.spec.js ├── users.spec.js └── utils.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "es2015-node5", "stage-0" ] } ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org # A special property that should be specified at the top of the file outside of # any sections. Set to true to stop .editor config file search on current file root = true [*] # Indentation style # Possible values - tab, space indent_style = space # Indentation size in single-spaced characters # Possible values - an integer, tab indent_size = 2 # Line ending file format # Possible values - lf, crlf, cr end_of_line = lf # File character encoding # Possible values - latin1, utf-8, utf-16be, utf-16le charset = utf-8 # Denotes whether to trim whitespace at the end of lines # Possible values - true, false trim_trailing_whitespace = true # Denotes whether file should end with a newline # Possible values - true, false insert_final_newline = true ================================================ FILE: .eslintrc.json ================================================ { "parser": "babel-eslint", "extends": "standard", "env": { "node": true, "mocha": true } } ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/node,sublimetext ### Node ### # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory node_modules # Optional npm cache directory .npm # Optional REPL history .node_repl_history ### SublimeText ### # cache files for sublime text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # workspace files are user-specific *.sublime-workspace # project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using SublimeText *.sublime-project # sftp configuration file sftp-config.json #Documentation docs ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Adrian Obelmejias Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ #koa2-api-boilerplate [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) Boilerplate for building APIs with [koa2](https://github.com/koajs/koa/tree/v2.x) and mongodb. This project covers basic necessities of most APIs. * Authentication (passport & jwt) * Database (mongoose) * Testing (mocha) * Doc generation with apidoc * linting using standard ##Requirements * node __^4.0.0__ * npm __^3.0.0__ ##Installation ```bash git clone https://github.com/adrianObel/koa2-api-boilerplate.git ``` ##Features * [koa2](https://github.com/koajs/koa/tree/v2.x) * [koa-router](https://github.com/alexmingoia/koa-router) * [koa-bodyparser](https://github.com/koajs/bodyparser) * [koa-generic-session](https://github.com/koajs/generic-session) * [koa-logger](https://github.com/koajs/logger) * [MongoDB](http://mongodb.org/) * [Mongoose](http://mongoosejs.com/) * [Passport](http://passportjs.org/) * [Nodemon](http://nodemon.io/) * [Mocha](https://mochajs.org/) * [apidoc](http://apidocjs.com/) * [Babel](https://github.com/babel/babel) * [ESLint](http://eslint.org/) ##Structure ``` ├── bin │ └── server.js # Bootstrapping and entry point ├── config # Server configuration settings │ ├── env # Environment specific config │ │ ├── common.js │ │ ├── development.js │ │ ├── production.js │ │ └── test.js │ ├── index.js # Config entrypoint - exports config according to envionrment and commons │ └── passport.js # Passportjs config of strategies ├── src # Source code │ ├── modules │ │ ├── controller.js # Module-specific controllers │ │ └── router.js # Router definitions for module │ ├── models # Mongoose models │ └── middleware # Custom middleware │ └── validators # Validation middleware └── test # Unit tests ``` ##Usage * `npm start` Start server on live mode * `npm run dev` Start server on dev mode with nodemon * `npm run docs` Generate API documentation * `npm test` Run mocha tests ##Documentation API documentation is written inline and generated by [apidoc](http://apidocjs.com/). Visit `http://localhost:5000/docs/` to view docs ##License MIT ================================================ FILE: bin/server.js ================================================ import Koa from 'koa' import bodyParser from 'koa-bodyparser' import convert from 'koa-convert' import logger from 'koa-logger' import mongoose from 'mongoose' import session from 'koa-generic-session' import passport from 'koa-passport' import mount from 'koa-mount' import serve from 'koa-static' import config from '../config' import { errorMiddleware } from '../src/middleware' const app = new Koa() app.keys = [config.session] mongoose.Promise = global.Promise mongoose.connect(config.database) app.use(convert(logger())) app.use(bodyParser()) app.use(session()) app.use(errorMiddleware()) app.use(convert(mount('/docs', serve(`${process.cwd()}/docs`)))) require('../config/passport') app.use(passport.initialize()) app.use(passport.session()) const modules = require('../src/modules') modules(app) app.listen(config.port, () => { console.log(`Server started on ${config.port}`) }) export default app ================================================ FILE: config/env/common.js ================================================ export default { port: process.env.PORT || 5000 } ================================================ FILE: config/env/development.js ================================================ export default { session: 'secret-boilerplate-token', token: 'secret-jwt-token', database: 'mongodb://localhost:27017/koa2-boilerplate-dev' } ================================================ FILE: config/env/production.js ================================================ export default { session: 'secret-boilerplate-token', token: 'secret-jwt-token', database: 'mongodb://localhost:27017/koa2-boilerplate-prod' } ================================================ FILE: config/env/test.js ================================================ export default { session: 'secret-boilerplate-token', token: 'secret-jwt-token', database: 'mongodb://localhost:27017/koa2-boilerplate-test' } ================================================ FILE: config/index.js ================================================ import common from './env/common' const env = process.env.NODE_ENV || 'development' const config = require(`./env/${env}`).default export default Object.assign({}, common, config) ================================================ FILE: config/passport.js ================================================ import passport from 'koa-passport' import User from '../src/models/users' import { Strategy } from 'passport-local' passport.serializeUser((user, done) => { done(null, user.id) }) passport.deserializeUser(async (id, done) => { try { const user = await User.findById(id, '-password') done(null, user) } catch (err) { done(err) } }) passport.use('local', new Strategy({ usernameField: 'username', passwordField: 'password' }, async (username, password, done) => { try { const user = await User.findOne({ username }) if (!user) { return done(null, false) } try { const isMatch = await user.validatePassword(password) if (!isMatch) { return done(null, false) } done(null, user) } catch (err) { done(err) } } catch (err) { return done(err) } })) ================================================ FILE: index.js ================================================ require('babel-core/register')() require('babel-polyfill') require('./bin/server.js') ================================================ FILE: package.json ================================================ { "name": "koa2-api-boilerplate", "version": "2.2.0", "description": "Koa2 boilerplate covering essentials for APIs", "main": "index.js", "scripts": { "start": "node index.js", "dev": "./node_modules/.bin/nodemon index.js", "test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-register --require babel-polyfill", "lint": "eslint src/**/*.js", "docs": "./node_modules/.bin/apidoc -i src/ -o docs" }, "keywords": [ "koa2-api-boilerplate", "api", "koa", "koa2", "boilerplate", "es6", "mongoose", "passportjs", "apidoc" ], "author": "Adrian Obelmejias ", "license": "MIT", "apidoc": { "title": "koa2-api-boilerplate", "url": "localhost:5000" }, "repository": { "type": "git", "url": "https://github.com/adrianObel/koa2-api-boilerplate" }, "dependencies": { "apidoc": "^0.16.1", "babel-core": "^6.5.1", "babel-polyfill": "^6.5.0", "babel-preset-es2015-node5": "^1.2.0", "babel-preset-stage-0": "^6.5.0", "bcrypt": "^0.8.5", "glob": "^7.0.0", "jsonwebtoken": "^7.1.9", "koa": "^2.0.0-alpha.6", "koa-bodyparser": "^3.0.0", "koa-convert": "^1.2.0", "koa-generic-session": "^1.10.1", "koa-logger": "^2.0.0", "koa-mount": "^1.3.0", "koa-passport": "^2.0.1", "koa-router": "^7.0.1", "koa-static": "^2.0.0", "mongoose": "^4.4.3", "passport-local": "^1.0.0" }, "devDependencies": { "babel-eslint": "^6.0.2", "babel-register": "^6.5.1", "chai": "^3.5.0", "eslint": "^3.4.0", "eslint-config-standard": "^6.0.0", "eslint-plugin-promise": "^2.0.1", "eslint-plugin-standard": "^2.0.0", "mocha": "^3.0.2", "nodemon": "^1.8.1", "supertest": "^2.0.0" } } ================================================ FILE: src/middleware/index.js ================================================ export function errorMiddleware () { return async (ctx, next) => { try { await next() } catch (err) { ctx.status = err.status || 500 ctx.body = err.message ctx.app.emit('error', err, ctx) } } } ================================================ FILE: src/middleware/validators.js ================================================ import User from '../models/users' import config from '../../config' import { getToken } from '../utils/auth' import { verify } from 'jsonwebtoken' export async function ensureUser (ctx, next) { const token = getToken(ctx) if (!token) { ctx.throw(401) } let decoded = null try { decoded = verify(token, config.token) } catch (err) { ctx.throw(401) } ctx.state.user = await User.findById(decoded.id, '-password') if (!ctx.state.user) { ctx.throw(401) } return next() } ================================================ FILE: src/models/users.js ================================================ import mongoose from 'mongoose' import bcrypt from 'bcrypt' import config from '../../config' import jwt from 'jsonwebtoken' const User = new mongoose.Schema({ type: { type: String, default: 'User' }, name: { type: String }, username: { type: String, required: true, unique: true }, password: { type: String, required: true } }) User.pre('save', function preSave (next) { const user = this if (!user.isModified('password')) { return next() } new Promise((resolve, reject) => { bcrypt.genSalt(10, (err, salt) => { if (err) { return reject(err) } resolve(salt) }) }) .then(salt => { bcrypt.hash(user.password, salt, (err, hash) => { if (err) { throw new Error(err) } user.password = hash next(null) }) }) .catch(err => next(err)) }) User.methods.validatePassword = function validatePassword (password) { const user = this return new Promise((resolve, reject) => { bcrypt.compare(password, user.password, (err, isMatch) => { if (err) { return reject(err) } resolve(isMatch) }) }) } User.methods.generateToken = function generateToken () { const user = this return jwt.sign({ id: user.id }, config.token) } export default mongoose.model('user', User) ================================================ FILE: src/modules/auth/controller.js ================================================ import passport from 'koa-passport' /** * @apiDefine TokenError * @apiError Unauthorized Invalid JWT token * * @apiErrorExample {json} Unauthorized-Error: * HTTP/1.1 401 Unauthorized * { * "status": 401, * "error": "Unauthorized" * } */ /** * @api {post} /auth Authenticate user * @apiVersion 1.0.0 * @apiName AuthUser * @apiGroup Auth * * @apiParam {String} username User username. * @apiParam {String} password User password. * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X POST -d '{ "username": "johndoe@gmail.com", "password": "foo" }' localhost:5000/auth * * @apiSuccess {Object} user User object * @apiSuccess {ObjectId} user._id User id * @apiSuccess {String} user.name User name * @apiSuccess {String} user.username User username * @apiSuccess {String} token Encoded JWT * * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK * { * "user": { * "_id": "56bd1da600a526986cf65c80" * "username": "johndoe" * }, * "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" * } * * @apiError Unauthorized Incorrect credentials * * @apiErrorExample {json} Error-Response: * HTTP/1.1 401 Unauthorized * { * "status": 401, * "error": "Unauthorized" * } */ export async function authUser (ctx, next) { return passport.authenticate('local', (user) => { if (!user) { ctx.throw(401) } const token = user.generateToken() const response = user.toJSON() delete response.password ctx.body = { token, user: response } })(ctx, next) } ================================================ FILE: src/modules/auth/router.js ================================================ import * as auth from './controller' export const baseUrl = '/auth' export default [ { method: 'POST', route: '/', handlers: [ auth.authUser ] } ] ================================================ FILE: src/modules/index.js ================================================ import glob from 'glob' import Router from 'koa-router' exports = module.exports = function initModules (app) { glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => { if (err) { throw err } matches.forEach((mod) => { const router = require(`${mod}/router`) const routes = router.default const baseUrl = router.baseUrl const instance = new Router({ prefix: baseUrl }) routes.forEach((config) => { const { method = '', route = '', handlers = [] } = config const lastHandler = handlers.pop() instance[method.toLowerCase()](route, ...handlers, async function(ctx) { return await lastHandler(ctx) }) app .use(instance.routes()) .use(instance.allowedMethods()) }) }) }) } ================================================ FILE: src/modules/users/controller.js ================================================ import User from '../../models/users' /** * @api {post} /users Create a new user * @apiPermission * @apiVersion 1.0.0 * @apiName CreateUser * @apiGroup Users * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X POST -d '{ "user": { "username": "johndoe", "password": "secretpasas" } }' localhost:5000/users * * @apiParam {Object} user User object (required) * @apiParam {String} user.username Username. * @apiParam {String} user.password Password. * * @apiSuccess {Object} users User object * @apiSuccess {ObjectId} users._id User id * @apiSuccess {String} users.name User name * @apiSuccess {String} users.username User username * * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK * { * "user": { * "_id": "56bd1da600a526986cf65c80" * "name": "John Doe" * "username": "johndoe" * } * } * * @apiError UnprocessableEntity Missing required parameters * * @apiErrorExample {json} Error-Response: * HTTP/1.1 422 Unprocessable Entity * { * "status": 422, * "error": "Unprocessable Entity" * } */ export async function createUser (ctx) { const user = new User(ctx.request.body.user) try { await user.save() } catch (err) { ctx.throw(422, err.message) } const token = user.generateToken() const response = user.toJSON() delete response.password ctx.body = { user: response, token } } /** * @api {get} /users Get all users * @apiPermission user * @apiVersion 1.0.0 * @apiName GetUsers * @apiGroup Users * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X GET localhost:5000/users * * @apiSuccess {Object[]} users Array of user objects * @apiSuccess {ObjectId} users._id User id * @apiSuccess {String} users.name User name * @apiSuccess {String} users.username User username * * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK * { * "users": [{ * "_id": "56bd1da600a526986cf65c80" * "name": "John Doe" * "username": "johndoe" * }] * } * * @apiUse TokenError */ export async function getUsers (ctx) { const users = await User.find({}, '-password') ctx.body = { users } } /** * @api {get} /users/:id Get user by id * @apiPermission user * @apiVersion 1.0.0 * @apiName GetUser * @apiGroup Users * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X GET localhost:5000/users/56bd1da600a526986cf65c80 * * @apiSuccess {Object} users User object * @apiSuccess {ObjectId} users._id User id * @apiSuccess {String} users.name User name * @apiSuccess {String} users.username User username * * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK * { * "user": { * "_id": "56bd1da600a526986cf65c80" * "name": "John Doe" * "username": "johndoe" * } * } * * @apiUse TokenError */ export async function getUser (ctx, next) { try { const user = await User.findById(ctx.params.id, '-password') if (!user) { ctx.throw(404) } ctx.body = { user } } catch (err) { if (err === 404 || err.name === 'CastError') { ctx.throw(404) } ctx.throw(500) } if (next) { return next() } } /** * @api {put} /users/:id Update a user * @apiPermission * @apiVersion 1.0.0 * @apiName UpdateUser * @apiGroup Users * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X PUT -d '{ "user": { "name": "Cool new Name" } }' localhost:5000/users/56bd1da600a526986cf65c80 * * @apiParam {Object} user User object (required) * @apiParam {String} user.name Name. * @apiParam {String} user.username Username. * * @apiSuccess {Object} users User object * @apiSuccess {ObjectId} users._id User id * @apiSuccess {String} users.name Updated name * @apiSuccess {String} users.username Updated username * * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK * { * "user": { * "_id": "56bd1da600a526986cf65c80" * "name": "Cool new name" * "username": "johndoe" * } * } * * @apiError UnprocessableEntity Missing required parameters * * @apiErrorExample {json} Error-Response: * HTTP/1.1 422 Unprocessable Entity * { * "status": 422, * "error": "Unprocessable Entity" * } * * @apiUse TokenError */ export async function updateUser (ctx) { const user = ctx.body.user Object.assign(user, ctx.request.body.user) await user.save() ctx.body = { user } } /** * @api {delete} /users/:id Delete a user * @apiPermission * @apiVersion 1.0.0 * @apiName DeleteUser * @apiGroup Users * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X DELETE localhost:5000/users/56bd1da600a526986cf65c80 * * @apiSuccess {StatusCode} 200 * * @apiSuccessExample {json} Success-Response: * HTTP/1.1 200 OK * { * "success": true * } * * @apiUse TokenError */ export async function deleteUser (ctx) { const user = ctx.body.user await user.remove() ctx.status = 200 ctx.body = { success: true } } ================================================ FILE: src/modules/users/router.js ================================================ import { ensureUser } from '../../middleware/validators' import * as user from './controller' export const baseUrl = '/users' export default [ { method: 'POST', route: '/', handlers: [ user.createUser ] }, { method: 'GET', route: '/', handlers: [ ensureUser, user.getUsers ] }, { method: 'GET', route: '/:id', handlers: [ ensureUser, user.getUser ] }, { method: 'PUT', route: '/:id', handlers: [ ensureUser, user.getUser, user.updateUser ] }, { method: 'DELETE', route: '/:id', handlers: [ ensureUser, user.getUser, user.deleteUser ] } ] ================================================ FILE: src/utils/auth.js ================================================ export function getToken (ctx) { const header = ctx.request.header.authorization if (!header) { return null } const parts = header.split(' ') if (parts.length !== 2) { return null } const scheme = parts[0] const token = parts[1] if (/^Bearer$/i.test(scheme)) { return token } return null } ================================================ FILE: test/auth.spec.js ================================================ import app from '../bin/server' import supertest from 'supertest' import { expect, should } from 'chai' import { cleanDb, authUser } from './utils' should() const request = supertest.agent(app.listen()) const context = {} describe('Auth', () => { before((done) => { cleanDb() authUser(request, (err, { user, token }) => { if (err) { return done(err) } context.user = user context.token = token done() }) }) describe('POST /auth', () => { it('should throw 401 if credentials are incorrect', (done) => { request .post('/auth') .set('Accept', 'application/json') .send({ username: 'supercoolname', password: 'wrongpassword' }) .expect(401, done) }) it('should auth user', (done) => { request .post('/auth') .set('Accept', 'application/json') .send({ username: 'test', password: 'pass' }) .expect(200, (err, res) => { if (err) { return done(err) } res.body.user.should.have.property('username') res.body.user.username.should.equal('test') expect(res.body.user.password).to.not.exist context.user = res.body.user context.token = res.body.token done() }) }) }) }) ================================================ FILE: test/users.spec.js ================================================ import app from '../bin/server' import supertest from 'supertest' import { expect, should } from 'chai' import { cleanDb } from './utils' should() const request = supertest.agent(app.listen()) const context = {} describe('Users', () => { before((done) => { cleanDb() done() }) describe('POST /users', () => { it('should reject signup when data is incomplete', (done) => { request .post('/users') .set('Accept', 'application/json') .send({ username: 'supercoolname' }) .expect(422, done) }) it('should sign up', (done) => { request .post('/users') .set('Accept', 'application/json') .send({ user: { username: 'supercoolname', password: 'supersecretpassword' } }) .expect(200, (err, res) => { if (err) { return done(err) } res.body.user.should.have.property('username') res.body.user.username.should.equal('supercoolname') expect(res.body.user.password).to.not.exist context.user = res.body.user context.token = res.body.token done() }) }) }) describe('GET /users', () => { it('should not fetch users if the authorization header is missing', (done) => { request .get('/users') .set('Accept', 'application/json') .expect(401, done) }) it('should not fetch users if the authorization header is missing the scheme', (done) => { request .get('/users') .set({ Accept: 'application/json', Authorization: '1' }) .expect(401, done) }) it('should not fetch users if the authorization header has invalid scheme', (done) => { const { token } = context request .get('/users') .set({ Accept: 'application/json', Authorization: `Unknown ${token}` }) .expect(401, done) }) it('should not fetch users if token is invalid', (done) => { request .get('/users') .set({ Accept: 'application/json', Authorization: 'Bearer 1' }) .expect(401, done) }) it('should fetch all users', (done) => { const { token } = context request .get('/users') .set({ Accept: 'application/json', Authorization: `Bearer ${token}` }) .expect(200, (err, res) => { if (err) { return done(err) } res.body.should.have.property('users') res.body.users.should.have.length(1) done() }) }) }) describe('GET /users/:id', () => { it('should not fetch user if token is invalid', (done) => { request .get('/users/1') .set({ Accept: 'application/json', Authorization: 'Bearer 1' }) .expect(401, done) }) it('should throw 404 if user doesn\'t exist', (done) => { const { token } = context request .get('/users/1') .set({ Accept: 'application/json', Authorization: `Bearer ${token}` }) .expect(404, done) }) it('should fetch user', (done) => { const { user: { _id }, token } = context request .get(`/users/${_id}`) .set({ Accept: 'application/json', Authorization: `Bearer ${token}` }) .expect(200, (err, res) => { if (err) { return done(err) } res.body.should.have.property('user') expect(res.body.user.password).to.not.exist done() }) }) }) describe('PUT /users/:id', () => { it('should not update user if token is invalid', (done) => { request .put('/users/1') .set({ Accept: 'application/json', Authorization: 'Bearer 1' }) .expect(401, done) }) it('should throw 404 if user doesn\'t exist', (done) => { const { token } = context request .put('/users/1') .set({ Accept: 'application/json', Authorization: `Bearer ${token}` }) .expect(404, done) }) it('should update user', (done) => { const { user: { _id }, token } = context request .put(`/users/${_id}`) .set({ Accept: 'application/json', Authorization: `Bearer ${token}` }) .send({ user: { username: 'updatedcoolname' } }) .expect(200, (err, res) => { if (err) { return done(err) } res.body.user.should.have.property('username') res.body.user.username.should.equal('updatedcoolname') expect(res.body.user.password).to.not.exist done() }) }) }) describe('DELETE /users/:id', () => { it('should not delete user if token is invalid', (done) => { request .delete('/users/1') .set({ Accept: 'application/json', Authorization: 'Bearer 1' }) .expect(401, done) }) it('should throw 404 if user doesn\'t exist', (done) => { const { token } = context request .delete('/users/1') .set({ Accept: 'application/json', Authorization: `Bearer ${token}` }) .expect(404, done) }) it('should delete user', (done) => { const { user: { _id }, token } = context request .delete(`/users/${_id}`) .set({ Accept: 'application/json', Authorization: `Bearer ${token}` }) .expect(200, done) }) }) }) ================================================ FILE: test/utils.js ================================================ import mongoose from 'mongoose' export function cleanDb () { for (const collection in mongoose.connection.collections) { if (mongoose.connection.collections.hasOwnProperty(collection)) { mongoose.connection.collections[collection].remove() } } } export function authUser (agent, callback) { agent .post('/users') .set('Accept', 'application/json') .send({ user: { username: 'test', password: 'pass' } }) .end((err, res) => { if (err) { return callback(err) } callback(null, { user: res.body.user, token: res.body.token }) }) }