├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── actions ├── checkExists.js ├── create.js ├── find.js ├── findAndPaginate.js ├── findById.js ├── findOneAndUpdate.js ├── remove.js └── update.js ├── config ├── db.js ├── server.js └── template.js ├── docker-compose.yml ├── helpers ├── body-parser.js ├── error.js ├── excel.js ├── facebook.js ├── jwt.js ├── logger.js ├── mailer.js ├── prerender.js ├── request.js └── validation.js ├── index.js ├── modules └── v1 │ ├── auth │ ├── controller.js │ ├── routes.js │ └── validators.js │ ├── routes.js │ ├── setup │ ├── controller.js │ ├── model.js │ ├── routes.js │ └── validators.js │ └── user │ ├── controller.js │ ├── model.js │ ├── routes.js │ └── validators.js ├── package.json ├── public ├── assets │ └── img │ │ ├── logo.png │ │ ├── no_image.png │ │ ├── not_found.png │ │ ├── reset-password.jpg │ │ └── updated-password.jpg └── files │ └── .gitkeep ├── robots.txt ├── seeds ├── config │ └── index.js ├── index.js ├── seeder.js ├── setup │ └── index.js └── user │ └── index.js ├── templates ├── mails │ ├── auth │ │ ├── reset-password.pug │ │ └── updated-password.pug │ ├── base-layout.pug │ ├── contact │ │ └── new.pug │ └── error │ │ └── index.pug └── sheets │ └── test.pug ├── tests ├── config │ ├── app.js │ └── chai.js ├── helpers │ ├── auth.js │ ├── request.js │ └── routes.js ├── integration │ └── modules │ │ └── v1 │ │ ├── auth.spec.js │ │ └── user.spec.js └── unit │ └── modules │ └── v1 │ ├── routes.spec.js │ └── user │ ├── controller.spec.js │ ├── routes.spec.js │ └── validators.spec.js ├── tmp └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=example 2 | APP_DISPLAY_NAME=Example 3 | APP_SECRET=RANDOMHASH 4 | 5 | DB_HOST=mongodb://localhost/NOME DO SEU DB 6 | DB_HOST_TEST=mongodb://localhost/NOME DO SEU DB 7 | 8 | PORT=3000 9 | PORT_TEST=3002 10 | CONTACT_EMAIL=contact@example.com 11 | 12 | FB_ID= 13 | FB_SECRET= 14 | FB_TOKEN= 15 | FB_PAGE= 16 | 17 | MAIL_ACCOUNT_NAME=Example User 18 | MAIL_HOST=smtp.gmail.com 19 | MAIL_PORT=465 20 | MAIL_USER= 21 | MAIL_PASSWORD= 22 | MAIL_DEV_FALLBACK=developeremail@developeremail.com 23 | 24 | PRERENDER_TOKEN= 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tmp/**/*.js 2 | node_modules/**/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'standard', 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | rules: { 8 | // allow paren-less arrow functions 9 | 'arrow-parens': 0, 10 | // allow async-await 11 | 'generator-star-spacing': 0, 12 | // allow debugger during development 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 14 | }, 15 | env: { 16 | node: true, 17 | mongo: true, 18 | mocha: true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Convert text file line endings to lf 2 | * text=auto 3 | 4 | *.js text eol=lf 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # Environment files 37 | .env 38 | 39 | # Other stuff 40 | .DS_Store 41 | public/files/**/* 42 | !public/**/no_image.* 43 | tmp/* 44 | deploy.sh 45 | 46 | 47 | 48 | !.gitkeep 49 | !**/.gitkeep 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | # language: node_js 4 | services: 5 | - docker 6 | env: 7 | - DOCKER_COMPOSE_VERSION=1.8.0 8 | # before_install: 9 | # - sudo apt-get update 10 | # - sudo apt-get install -o Dpkg::Options::="--force-confold" --force-yes -y docker-engine 11 | # - docker-compose --version 12 | # - sudo rm /usr/local/bin/docker-compose 13 | # - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 14 | # - chmod +x docker-compose 15 | # - sudo mv docker-compose /usr/local/bin 16 | # - docker-compose --version 17 | # 18 | # # Setup your application stack. You may need to tweak these commands if you 19 | # # doing out-of-the-ordinary docker-compose builds. 20 | # - docker-compose pull 21 | # - docker-compose build 22 | # - docker-compose start 23 | # 24 | # - docker ps 25 | # 26 | 27 | before_install: 28 | - sudo apt-get update 29 | - sudo rm /usr/local/bin/docker-compose 30 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 31 | - chmod +x docker-compose 32 | - sudo mv docker-compose /usr/local/bin 33 | - docker-compose --version 34 | - docker-compose pull 35 | - docker-compose build 36 | 37 | before_script: 38 | - docker-compose up -d 39 | 40 | script: 41 | - docker-compose exec api yarn 42 | - docker-compose exec api yarn run test 43 | 44 | after_script: 45 | - docker-compose down 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gustavo Viegas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/gfviegas/scout-cms.svg?branch=master)](https://travis-ci.org/gfviegas/express-rest-api) 2 | [![Dependency Status](https://gemnasium.com/badges/github.com/gfviegas/express-rest-api.svg)](https://gemnasium.com/github.com/gfviegas/express-rest-api) 3 | [![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | # API RESTful em Express, Mongoose, Mocha e Chai. 5 | 6 | # Scripts 7 | 8 | Os seguintes scripts estão disponiveis com `yarn run SCRIPT` ou `npm run SCRIPT` 9 | 10 | | Script | Descrição | 11 | | ------ | ------ | 12 | | test | Roda o linter, roda os testes unitários e os testes de integração, em sequência | 13 | | start | Inicia o servidor com hot auto-reload utilizando o nodemon | 14 | | dev | Inicia o servidor de desenvolvimento com hot auto-reload utilizando o nodemon | 15 | | dev-win | Inicia o servidor de desenvolvimento com hot auto-reload utilizando o nodemon em legacy mode para windows | 16 | | lint | Roda o ESLINT para conferir o styleguide do código | 17 | | prod | Inicia o servidor de produção com hot auto-reload utilizando o nodemon | 18 | | seed | Alimenta o banco de dados através das estratégias na pasta seed | 19 | | test:integration | Roda apenas os testes de integração, uma única vez | 20 | | test:integration-server | Inicia o servidor de desenvolvimento dos testes de integração, com auto-reload | 21 | | test:unit | Roda apenas os testes unitários, uma única vez | 22 | | test:unit-server | Inicia o servidor de desenvolvimento dos testes unitários, com auto-reload | 23 | | test:report | Gera o relatório de cobertura dos testes | 24 | 25 | # Sobre este boilerplate 26 | 27 | ## Organização de Pastas 28 | Vamos seguir um padrão de organização de pastas para ficar cada coisa em seu lugar e clean. 29 | 30 | ### ./ 31 | Temos nosso entry point `index.js` que vai inicializar nossa aplicação. 32 | 33 | Também temos o nosso arquivo `.env` que abriga as variáveis de ambiente que usaremos 34 | 35 | ### ./config 36 | A pasta config abriga os scripts pra inicializar o server e a conexão do mongoose. A medida que precisarmos de novos scripts de configuração ou conexão da API, coloque-os aqui. 37 | 38 | ### ./actions 39 | Sabe aquele GET `api/v1/module` que só tem a função de dar um GET da entidade e retornar? Muitas vezes você vai utilizar métodos que são idênticos em múltiplos models. É pra isso que essa folder existe. Ela abriga pequenas **ações** reaproveitáveis ao invés de poluir os controllers com códigos iguais. Utilize quantas e quais sentir necessidade, a maioria das vezes serão operações com o mongoose. 40 | 41 | ### ./helpers 42 | Coloque aqui os arquivos helpers, funcões modularizadas que vão facilitar a manutenção do seu código. Diferente do actions as funções aqui abrigadas não tem relação com controllers, elas podem fazer simples operações como criptografar uma string ou calcular uma média. 43 | 44 | Divida os helpers em folders como achar conveniente (ex: `helpers/math` para operações matemáticas, `helpers/string` para manipulação de strings, etc). 45 | 46 | ### ./modules 47 | Aqui é onde vive a lógica e as rotas de sua API. Antes de cada módulo em si, precisamos versioná-los, abrigando as pastas `v1`, `v2`, e etc. 48 | 49 | ### ./modules/v1 50 | Módulos da versão 1 da nossa API. As APIs simples e com poucos clientes terão, na maioria das vezes, apenas uma versão. Ainda sim deveremos versioná-los. 51 | 52 | Pra cada módulo de nossa API, criaremos uma pasta aqui dentro. 53 | 54 | Além disso também temos um arquivo routes.js que "aplicam" as rotas dos módulos de sua versão. Por quê um routes.js por versão? Porque em uma determinada versão você pode não possuir uma rota X, ou possuir uma Y com nome diferente, ou até uma Z nova. É bom fazer as coisas dinâmicas, mas não dá pra pensar em muita mágica em uma situação dessas. 55 | 56 | 57 | ### ./modules/v1/nomeDoModulo 58 | Para cada módulo nós vamos possuir os seguintes files: 59 | #### routes.js 60 | É aqui que definimos nossos endpoints desse módulo. Alguns módulos não possuem criação ou delete por exemplo. Outros possuem alguns sub-documentos que devem ser servidos. Declare todas as rotas nesse arquivo. 61 | 62 | #### controller.js 63 | Quando cairmos nas rotas, o que a API deve fazer? Essa responsabilidade é de nosso controller, que vai utilizar actions comuns do mongoose e/ou outros métodos que você vai cadastrar nesse file. 64 | 65 | #### model.js 66 | Auto-explicativo. Defina aqui o schema e model do mongoose de seu module, se tiver algum. 67 | 68 | #### validators.js 69 | Você diversas vezes vai precisar validar parâmetros de criação e edição de dados, além de algumas lógicas customizadas (Ex: não pode possuir mais de uma moto vermelha no sistema). É aqui que você vai criar essas validações. Elas rodam no formato de middleware do express, o que significa que a sua rota só vai chegar ao controller se passar no validator cadastrado. 70 | -------------------------------------------------------------------------------- /actions/checkExists.js: -------------------------------------------------------------------------------- 1 | module.exports = (Model) => { 2 | return (req, res) => { 3 | Model 4 | .count(req.body) 5 | .exec((err, value) => { 6 | if (err) throw err 7 | res.status(200).json({exists: !!(value >= 1)}) 8 | }) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /actions/create.js: -------------------------------------------------------------------------------- 1 | module.exports = (Model) => { 2 | return (req, res) => { 3 | const data = req.body 4 | const modelInstance = new Model(data) 5 | 6 | modelInstance.save((err, data) => { 7 | if (err) throw err 8 | 9 | res.status(201).json(modelInstance) 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /actions/find.js: -------------------------------------------------------------------------------- 1 | module.exports = (model) => { 2 | return (req, res) => { 3 | const query = (req.query) || {} 4 | model 5 | .find(query) 6 | .sort({'created_at': '-1'}) 7 | .exec((err, data) => { 8 | if (err) throw err 9 | res.status(200).json(data) 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /actions/findAndPaginate.js: -------------------------------------------------------------------------------- 1 | module.exports = (model) => { 2 | return (req, res) => { 3 | const query = {} 4 | const pagOptions = { 5 | page: (req.query.page - 1) || 0, 6 | limit: req.query.limit || 15 7 | } 8 | 9 | const operation = model.find(query) 10 | const meta = { 11 | meta: { 12 | currentPage: pagOptions.page, 13 | limit: pagOptions.limit, 14 | totalPages: Math.ceil(operation.count() / pagOptions.limit) 15 | } 16 | } 17 | 18 | operation 19 | .sort({'created_at': '-1'}) 20 | .skip(pagOptions.page * pagOptions.limit) 21 | .limit(pagOptions.limit) 22 | .exec((err, data) => { 23 | if (err) throw err 24 | 25 | const response = Object.assign(data, meta) 26 | res.status(200).json(response) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /actions/findById.js: -------------------------------------------------------------------------------- 1 | module.exports = (Model) => { 2 | return (req, res) => { 3 | const id = {_id: req.params.id} 4 | Model.findById(id, (err, data) => { 5 | if (err) throw err 6 | 7 | if (data) { 8 | res.status(200).json(data) 9 | } else { 10 | res.status(404).json({}) 11 | } 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /actions/findOneAndUpdate.js: -------------------------------------------------------------------------------- 1 | module.exports = (Model) => { 2 | return async (req, res) => { 3 | try { 4 | const mod = req.body 5 | let entity = await Model.findById(req.params.id).exec() 6 | if (!entity) return res.status(404).json() 7 | entity = Object.assign(entity, mod) 8 | 9 | await entity.save() 10 | return res.status(200).json(entity) 11 | } catch (e) { 12 | throw e 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /actions/remove.js: -------------------------------------------------------------------------------- 1 | module.exports = (Model) => { 2 | return async (req, res) => { 3 | try { 4 | const entity = await Model.findById(req.params.id) 5 | if (!entity) return res.status(404).json() 6 | 7 | entity.remove() 8 | res.status(204).json() 9 | } catch (e) { 10 | throw e 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /actions/update.js: -------------------------------------------------------------------------------- 1 | module.exports = (Model) => { 2 | return (req, res) => { 3 | const query = {_id: req.params.id} 4 | const mod = req.body 5 | Model.update(query, mod, (err, data) => { 6 | if (err) throw err 7 | 8 | res.status(200).json(data) 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | mongoose.Promise = global.Promise 3 | 4 | const options = {} 5 | 6 | if (process.env.NODE_ENV === 'test') { 7 | mongoose.connect(process.env.DB_HOST_TEST, options) 8 | } else { 9 | mongoose.connect(process.env.DB_HOST, options) 10 | } 11 | 12 | const db = mongoose.connection 13 | 14 | if (process.env.NODE_ENV === 'development') { 15 | db.on('error', (err) => { 16 | console.log('DB connection error', err) 17 | }) 18 | db.on('open', () => { 19 | console.log('DB connection open') 20 | }) 21 | } 22 | 23 | db.on('connected', (err) => { 24 | if (err) throw err 25 | if (process.env.NODE_ENV === 'development') { console.log('DB connected successfully!') } 26 | }) 27 | 28 | db.on('disconnected', (err) => { 29 | if (err) throw err 30 | if (process.env.NODE_ENV === 'development') { console.log('DB disconnected') } 31 | }) 32 | 33 | module.exports = db 34 | -------------------------------------------------------------------------------- /config/server.js: -------------------------------------------------------------------------------- 1 | let port = process.env.PORT || 8080 2 | 3 | if (process.env.NODE_ENV === 'test') { 4 | port = process.env.PORT_TEST 5 | } 6 | 7 | const server = {} 8 | 9 | server.start = (app) => { 10 | app.listen(port, () => { 11 | if (process.env.NODE_ENV === 'development') { 12 | console.log('------------------------------------------------------------') 13 | console.log('Express server listening on port ' + port) 14 | console.log('------------------------------------------------------------') 15 | } 16 | }) 17 | } 18 | 19 | module.exports = server 20 | -------------------------------------------------------------------------------- /config/template.js: -------------------------------------------------------------------------------- 1 | const email = { 2 | base_url: process.env.SITE_URL, 3 | admin_url: process.env.ADMIN_URL, 4 | base_name: process.env.ADMIN_URL, 5 | contact: { 6 | email: process.env.CONTACT_EMAIL 7 | }, 8 | logo: `${process.env.ASSETS_URL}img/logo.png`, 9 | imgPath: `${process.env.ASSETS_URL}img/` 10 | } 11 | 12 | const configure = (app) => { 13 | app.set('view engine', 'pug') 14 | app.set('views', './templates') 15 | 16 | app.locals._email = email 17 | } 18 | 19 | module.exports = { 20 | email: email, 21 | configure: configure 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | mongo: 5 | container_name: example-mongo 6 | image: mongo 7 | ports: 8 | - "27017:27017" 9 | volumes_from: 10 | - mongo_data 11 | 12 | mongo_data: 13 | container_name: example-mongo-data 14 | image: tianon/true 15 | volumes: 16 | - ./tmp/db:/data/db 17 | - ./tmp/backups:/data/backups 18 | 19 | api: 20 | image: node:8 21 | container_name: example-api 22 | working_dir: /var/www/app 23 | command: bash -c 'yarn && yarn run dev' 24 | volumes: 25 | - ./:/var/www/app 26 | - ./node_modules:/var/www/app/node_modules 27 | ports: 28 | - 3000:3000 29 | links: 30 | - mongo 31 | depends_on: 32 | - mongo 33 | -------------------------------------------------------------------------------- /helpers/body-parser.js: -------------------------------------------------------------------------------- 1 | const Busboy = require('busboy') 2 | const bytes = require('bytes') 3 | const concat = require('concat-stream') 4 | const debug = require('debug')('busboy-body-parser') 5 | 6 | var HARDLIMIT = bytes('250mb') 7 | 8 | module.exports = function (settings) { 9 | settings = settings || {} 10 | settings.limit = settings.limit || HARDLIMIT 11 | settings.multi = settings.multi || true 12 | 13 | if (typeof settings.limit === 'string') { 14 | settings.limit = bytes(settings.limit) 15 | } 16 | 17 | if (settings.limit > HARDLIMIT) { 18 | console.error('WARNING: busboy-body-parser file size limit set too high') 19 | console.error('busboy-body-parser can only handle files up to ' + HARDLIMIT + ' bytes') 20 | console.error('to handle larger files you should use a streaming solution.') 21 | settings.limit = HARDLIMIT 22 | } 23 | 24 | return function multipartBodyParser (req, res, next) { 25 | if (req.is('multipart/form-data')) { 26 | var busboy 27 | try { 28 | busboy = new Busboy({ 29 | headers: req.headers, 30 | limits: { 31 | fileSize: settings.limit 32 | } 33 | }) 34 | } catch (err) { 35 | return next(err) 36 | } 37 | busboy.on('field', function (key, value) { 38 | debug('Received field %s: %s', key, value) 39 | 40 | const regexCondition = value.match(/\{(.*)\}/) || value.match(/\[(.*)\]/) 41 | 42 | // is a json? 43 | if (regexCondition) { 44 | const objectValue = JSON.parse(value) 45 | 46 | req.body[key] = objectValue 47 | } else { 48 | req.body[key] = value 49 | } 50 | }) 51 | busboy.on('file', function (key, file, name, enc, mimetype) { 52 | file.pipe(concat(function (d) { 53 | var fileData = { 54 | data: file.truncated ? null : d, 55 | name: name, 56 | encoding: enc, 57 | mimetype: mimetype, 58 | truncated: file.truncated, 59 | size: file.truncated ? null : Buffer.byteLength(d, 'binary') 60 | } 61 | 62 | debug('Received file %s', file) 63 | 64 | if (settings.multi) { 65 | req.files[key] = req.files[key] || [] 66 | req.files[key].push(fileData) 67 | } else { 68 | req.files[key] = fileData 69 | } 70 | })) 71 | }) 72 | var error 73 | busboy.on('error', function (err) { 74 | debug('Error parsing form') 75 | debug(err) 76 | error = err 77 | next(err) 78 | }) 79 | busboy.on('finish', function () { 80 | if (error) { return } 81 | debug('Finished form parsing') 82 | debug(req.body) 83 | next() 84 | }) 85 | req.files = req.files || {} 86 | req.body = req.body || {} 87 | req.pipe(busboy) 88 | } else { 89 | next() 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /helpers/error.js: -------------------------------------------------------------------------------- 1 | const mailer = require('./mailer') 2 | 3 | const sendErrorMail = (data) => { 4 | const options = { 5 | to: '', 6 | subject: `⚠️ Erro em ${process.env.APP_DISPLAY_NAME} ⚠️`, 7 | template: { 8 | path: 'error/index', 9 | data: data 10 | } 11 | } 12 | mailer.sendMail(options) 13 | } 14 | 15 | module.exports = { 16 | sendMail: sendErrorMail 17 | } 18 | -------------------------------------------------------------------------------- /helpers/excel.js: -------------------------------------------------------------------------------- 1 | const Excel = require('exceljs') 2 | 3 | const defaultWorkbookOptions = { 4 | creator: process.env.APP_DISPLAY_NAME, 5 | created: new Date(), 6 | modified: new Date() 7 | } 8 | const defaultWorksheetOptions = { 9 | pageSetup: { 10 | verticalCentered: true, 11 | horizontalCentered: true 12 | } 13 | } 14 | 15 | const defaultStyleOptions = { 16 | font: { 17 | name: 'Arial', 18 | color: { argb: 'FF000000' }, 19 | family: 2, 20 | size: 10 21 | }, 22 | alignment: { vertical: 'middle', horizontal: 'center' }, 23 | border: { 24 | top: {style: 'thin'}, 25 | left: {style: 'thin'}, 26 | bottom: {style: 'thin'}, 27 | right: {style: 'thin'} 28 | } 29 | } 30 | 31 | const getStyle = (customOptions) => { 32 | let dflt = JSON.parse(JSON.stringify(defaultStyleOptions)) 33 | const style = Object.assign(dflt, customOptions) 34 | return style 35 | } 36 | 37 | const getHeaderStyle = (customOptions) => { 38 | const options = { 39 | font: { 40 | name: 'Arial', 41 | color: { argb: 'FFFFFFFF' }, 42 | family: 2, 43 | bold: true, 44 | size: 11 45 | }, 46 | fill: { 47 | type: 'pattern', 48 | pattern: 'solid', 49 | fgColor: {argb: 'FF006004'} 50 | }, 51 | border: { 52 | top: {style: 'double', color: {argb: 'FF00FF00'}}, 53 | left: {style: 'double', color: {argb: 'FF00FF00'}}, 54 | bottom: {style: 'double', color: {argb: 'FF00FF00'}}, 55 | right: {style: 'double', color: {argb: 'FF00FF00'}} 56 | }, 57 | height: 30 58 | } 59 | const style = Object.assign(options, customOptions) 60 | return style 61 | } 62 | 63 | const createWorkbook = (customOptions) => { 64 | const options = Object.assign(defaultWorkbookOptions, customOptions) 65 | const workbook = new Excel.Workbook() 66 | Object.keys(options).forEach(key => { 67 | workbook[key] = options[key] 68 | }) 69 | 70 | return workbook 71 | } 72 | 73 | const createWorksheet = (workbook, name, customOptions) => { 74 | const options = Object.assign(defaultWorksheetOptions, customOptions) 75 | const worksheet = workbook.addWorksheet(name, options) 76 | worksheet.properties.outlineLevelCol = 1 77 | worksheet.properties.outlineLevelRow = 1 78 | 79 | return worksheet 80 | } 81 | 82 | const exportWorkbook = (workbook, fileName) => { 83 | const filePath = `${process.cwd()}/tmp/${fileName}-${new Date().getTime()}.xlsx` 84 | return workbook.xlsx.writeFile(filePath) 85 | } 86 | 87 | const exportWorkbookDownload = (workbook, res, fileName) => { 88 | res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') 89 | res.setHeader('Content-Disposition', `attachment; filename=${fileName}-${new Date().getTime()}.xlsx`) 90 | return workbook.xlsx.write(res) 91 | } 92 | 93 | module.exports = { 94 | createWorkbook, 95 | createWorksheet, 96 | exportWorkbook, 97 | exportWorkbookDownload, 98 | getStyle, 99 | getHeaderStyle 100 | } 101 | -------------------------------------------------------------------------------- /helpers/facebook.js: -------------------------------------------------------------------------------- 1 | const FB = require('fb') 2 | 3 | const options = { 4 | appId: process.env.FB_ID, 5 | appSecret: process.env.FB_SECRET, 6 | accessToken: process.env.FB_TOKEN 7 | } 8 | 9 | FB.options(options) 10 | FB.setAccessToken(process.env.FB_TOKEN) 11 | 12 | module.exports = FB 13 | -------------------------------------------------------------------------------- /helpers/jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | 3 | const getTokenFromRequest = (req) => { 4 | const authorization = req.header('authorization') 5 | if (!authorization) { return false } 6 | return authorization.split(' ')[1] // Bearer 7 | } 8 | 9 | const getPayload = (req) => { 10 | const token = getTokenFromRequest(req) 11 | return jwt.decode(token) 12 | } 13 | 14 | const getUserId = (req) => { 15 | const data = getPayload(req) 16 | return data.sub 17 | } 18 | 19 | const generateToken = (user) => { 20 | const payload = { 21 | sub: user.id, 22 | data: { 23 | name: user.name 24 | } 25 | } 26 | 27 | const options = { 28 | expiresIn: '1d' 29 | } 30 | 31 | return jwt.sign(payload, process.env.APP_SECRET, options) 32 | } 33 | 34 | const middleware = (req, res, next) => { 35 | const token = getTokenFromRequest(req) 36 | if (token && token.length > 0) { 37 | jwt.verify(token, process.env.APP_SECRET, (err, decoded) => { 38 | if (err) { 39 | if (err.name === 'TokenExpiredError') { 40 | res.status(403).json({error: 'token_expired'}) 41 | } else { 42 | res.status(401).json({error: err.message.split(' ').join('_').toLowerCase()}) 43 | } 44 | return false 45 | } 46 | next() 47 | }) 48 | } else { 49 | res.status(401).json({error: 'no_token'}) 50 | return false 51 | } 52 | } 53 | 54 | module.exports = { 55 | getPayload, 56 | getUserId, 57 | getTokenFromRequest, 58 | generateToken, 59 | middleware 60 | } 61 | -------------------------------------------------------------------------------- /helpers/logger.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | 3 | const info = (msg) => { console.log(chalk.bold.blue(msg)) } 4 | const error = (msg) => { console.log(chalk.bold.red(msg)) } 5 | const success = (msg) => { console.log(chalk.bold.blue(msg)) } 6 | 7 | module.exports = { 8 | info, 9 | success, 10 | error 11 | } 12 | -------------------------------------------------------------------------------- /helpers/mailer.js: -------------------------------------------------------------------------------- 1 | const pug = require('pug') 2 | const emailData = require('../config/template').email 3 | const email = require('emailjs/email') 4 | 5 | const server = email.server.connect({ 6 | user: process.env.MAIL_USER, 7 | password: process.env.MAIL_PASSWORD, 8 | host: process.env.MAIL_HOST, 9 | port: process.env.MAIL_PORT, 10 | ssl: true 11 | }) 12 | 13 | const sendMail = (options) => { 14 | return new Promise((resolve, reject) => { 15 | const data = Object.assign({_email: emailData}, options.template.data) 16 | const mailPath = `${process.cwd()}/templates/mails/${options.template.path}.pug` 17 | const htmlstream = pug.renderFile(mailPath, data) 18 | const message = { 19 | from: `${process.env.MAIL_ACCOUNT_NAME} <${process.env.MAIL_HOST}>`, 20 | to: (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') ? process.env.MAIL_DEV_FALLBACK : (options.to), 21 | subject: options.subject, 22 | attachment: 23 | [ 24 | {data: htmlstream, alternative: true} 25 | ] 26 | } 27 | 28 | server.send(message, (error, info) => { 29 | if (error) { 30 | console.log(`[MAIL HELPER] Erro: ${JSON.stringify(error)}`) 31 | return reject(new Error({success: false, error: error})) 32 | } 33 | return resolve({success: true, info: info}) 34 | }) 35 | }) 36 | } 37 | 38 | module.exports = { 39 | sendMail 40 | } 41 | -------------------------------------------------------------------------------- /helpers/prerender.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise') 2 | 3 | const recache = (url) => { 4 | const options = { 5 | method: 'POST', 6 | uri: 'http://api.prerender.io/recache', 7 | body: { 8 | prerenderToken: process.env.PRERENDER_TOKEN, 9 | url: url 10 | }, 11 | json: true 12 | } 13 | return request('http://api.prerender.io/recache', options) 14 | } 15 | 16 | module.exports = { 17 | recache: recache 18 | } 19 | -------------------------------------------------------------------------------- /helpers/request.js: -------------------------------------------------------------------------------- 1 | 2 | const createQueryObject = (req) => { 3 | let reqQuery = Object.assign({}, req.query) 4 | if (reqQuery.hasOwnProperty('filter')) delete reqQuery.filter 5 | if (reqQuery.hasOwnProperty('page')) delete reqQuery.page 6 | if (reqQuery.hasOwnProperty('limit')) delete reqQuery.limit 7 | if (reqQuery.hasOwnProperty('sort')) delete reqQuery.sort 8 | return reqQuery 9 | } 10 | 11 | module.exports = { 12 | createQueryObject: createQueryObject 13 | } 14 | -------------------------------------------------------------------------------- /helpers/validation.js: -------------------------------------------------------------------------------- 1 | const handleValidation = (req, res, next) => { 2 | return req.getValidationResult() 3 | .then((result) => { 4 | if (!result.isEmpty()) { 5 | let errors = result.mapped() 6 | res.status(422).json(errors) 7 | return false 8 | } else { 9 | next() 10 | } 11 | }) 12 | .catch((error) => { 13 | throw error 14 | }) 15 | } 16 | 17 | module.exports = handleValidation 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Load environment vars and DB 2 | require('dotenv').config() 3 | require('./config/db') 4 | 5 | // Import packages 6 | const express = require('express') 7 | const path = require('path') 8 | const bodyParser = require('body-parser') 9 | const busboyBodyParser = require('./helpers/body-parser') 10 | const logger = require('morgan') 11 | const cors = require('cors') 12 | const validator = require('express-validator') 13 | const app = express() 14 | const server = require('./config/server') 15 | const versions = ['v1'] 16 | require('./config/template').configure(app) 17 | 18 | global._base = path.join(__dirname, '/') 19 | 20 | // Middlewares 21 | if (app.get('env') === 'development') { 22 | app.use(logger('dev')) 23 | app.get('/template', (req, res) => { 24 | res.render(`${req.query.path}`) 25 | }) 26 | } 27 | app.use(busboyBodyParser()) 28 | app.use(bodyParser.json()) 29 | app.use(bodyParser.urlencoded({ extended: false })) 30 | app.use(cors({ 31 | exposedHeaders: ['Content-Disposition'] 32 | })) 33 | app.use(validator()) 34 | app.use(express.static('public')) 35 | 36 | // Set global response headers 37 | app.use((req, res, next) => { 38 | res.setHeader('Content-Type', 'application/json') 39 | next() 40 | }) 41 | 42 | // Apply the routes for each version setted 43 | versions.forEach((version) => { 44 | const versionRoutes = require('./modules/' + version + '/routes') 45 | app.use('/api/' + version, versionRoutes) 46 | }) 47 | 48 | app.get('/', (req, res) => { 49 | res.status(200).json({}) 50 | }) 51 | 52 | // Start server 53 | server.start(app) 54 | 55 | module.exports = app 56 | -------------------------------------------------------------------------------- /modules/v1/auth/controller.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const actionsPath = './actions/' 3 | const Model = require('../user/model').model 4 | const extend = require('extend') 5 | const mailer = rfr('helpers/mailer') 6 | const jwtHelper = rfr('helpers/jwt') 7 | 8 | const controllerActions = {} 9 | 10 | // Import default actions 11 | const importActions = [] 12 | const createMethods = (element, index) => { 13 | controllerActions[element] = rfr(actionsPath + element)(Model) 14 | } 15 | importActions.forEach(createMethods) 16 | 17 | // Controller custom actions 18 | const customMethods = { 19 | fetch: (req, res) => { 20 | Model 21 | .findById(jwtHelper.getUserId(req), (err, data) => { 22 | if (err) throw err 23 | 24 | if (!data) return res.status(404).json() 25 | if (!data.active) return res.status(403).json({error: 'user_inactive'}) 26 | 27 | res.json(data) 28 | }) 29 | }, 30 | authenticate: (req, res) => { 31 | const query = {$or: [{username: req.body.username}, {email: req.body.username}]} 32 | Model.findOne(query) 33 | .select('+password') 34 | .exec((err, user) => { 35 | if (err) throw err 36 | 37 | if (!user) { 38 | return res.status(404).json({error: 'user_not_found'}) 39 | } else if (!user.active) { 40 | return res.status(403).json({error: 'user_inactive'}) 41 | } else { 42 | user.verifyPassword(req.body.password, (err, valid) => { 43 | if (err) throw err 44 | 45 | if (!valid) { 46 | return res.status(422).json({error: 'wrong_credentials'}) 47 | } else { 48 | return res.status(200).json({token: jwtHelper.generateToken(user)}) 49 | } 50 | }) 51 | } 52 | }) 53 | }, 54 | resetPassword: (req, res) => { 55 | req.connection.setTimeout(1000 * 60 * 10) 56 | const query = { email: req.body.email } 57 | Model 58 | .findOne(query) 59 | .exec((err, user) => { 60 | if (err) throw err 61 | 62 | if (!user) { 63 | res.status(404).json({error: 'user_not_found'}) 64 | } else { 65 | user['passwordToken'] = Math.random().toString(35).substr(2, 20).toUpperCase() 66 | 67 | user.save((error) => { 68 | if (error) throw error 69 | 70 | const options = { 71 | to: user['email'], 72 | subject: 'Alteração de Senha', 73 | template: { 74 | path: 'auth/reset-password', 75 | data: user.toObject() 76 | } 77 | } 78 | 79 | console.log(JSON.stringify(options)) 80 | return mailer.sendMail(options) 81 | .then(response => { 82 | res.status(204).json() 83 | }) 84 | .catch(response => { 85 | res.status(500).json(response) 86 | }) 87 | }) 88 | } 89 | }) 90 | }, 91 | findByPasswordToken: (req, res) => { 92 | const query = {passwordToken: req.params.token} 93 | Model 94 | .findOne(query, (err, data) => { 95 | if (err) throw err 96 | 97 | if (!data) { 98 | res.status(404).json({error: 'user_not_found'}) 99 | } else { 100 | res.status(200).json(data) 101 | } 102 | }) 103 | }, 104 | updatePassword: (req, res) => { 105 | const query = {passwordToken: req.params.token} 106 | Model 107 | .findOne(query, (err, user) => { 108 | if (err) throw err 109 | 110 | if (!user) { 111 | res.status(404).json({error: 'user_not_found'}) 112 | } else { 113 | user['passwordToken'] = undefined 114 | user['password'] = req.body.password 115 | user.save((error) => { 116 | if (error) throw error 117 | 118 | res.status(204).json() 119 | 120 | const options = { 121 | to: user['email'], 122 | subject: 'Senha Atualizada', 123 | template: { 124 | path: 'auth/updated-password', 125 | data: user.toObject() 126 | } 127 | } 128 | 129 | return mailer.sendMail(options) 130 | }) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | extend(controllerActions, customMethods) 137 | module.exports = controllerActions 138 | -------------------------------------------------------------------------------- /modules/v1/auth/routes.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const router = require('express').Router() 3 | const controller = require('./controller') 4 | const validators = require('./validators') 5 | const jwtMiddleware = rfr('/helpers/jwt').middleware 6 | 7 | // Get user by JWT 8 | router.get('/', [jwtMiddleware], controller.fetch) 9 | 10 | // Create JWT 11 | router.post('/', validators.create, controller.authenticate) 12 | 13 | // Set password reset token and send reset password email 14 | router.post('/reset', validators.reset, controller.resetPassword) 15 | 16 | // Get by Passwork Token 17 | router.get('/passwordtoken/:token', [], controller.findByPasswordToken) 18 | 19 | // Update the user password 20 | router.patch('/:token/password', validators.updatePassword, controller.updatePassword) 21 | 22 | module.exports = router 23 | -------------------------------------------------------------------------------- /modules/v1/auth/validators.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const handleValidation = rfr('/helpers/validation') 3 | 4 | const emailValidators = (req) => { 5 | req.checkBody('email', {error: 'required'}).notEmpty() 6 | req.checkBody('email', {error: 'invalid'}).isEmail() 7 | } 8 | const usernameValidators = (req) => { 9 | req.checkBody('username', {error: 'required'}).notEmpty() 10 | } 11 | const passwordValidators = (req) => { 12 | req.checkBody('password', {error: 'required'}).notEmpty() 13 | } 14 | const authValidator = (req) => { 15 | passwordValidators(req) 16 | usernameValidators(req) 17 | } 18 | const resetValidator = (req) => { 19 | emailValidators(req) 20 | } 21 | const updateValidator = (req) => { 22 | passwordValidators(req) 23 | } 24 | 25 | module.exports = { 26 | create: (req, res, next) => { 27 | authValidator(req) 28 | handleValidation(req, res, next) 29 | }, 30 | reset: (req, res, next) => { 31 | resetValidator(req) 32 | handleValidation(req, res, next) 33 | }, 34 | updatePassword: (req, res, next) => { 35 | updateValidator(req) 36 | handleValidation(req, res, next) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/v1/routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | 3 | /** 4 | * Use the modules routes. It's safer doing in a separate file than magically, to 5 | * be sure nester routes will be applied correctly. 6 | */ 7 | router.use('/auth', require('./auth/routes')) 8 | router.use('/setup', require('./setup/routes')) 9 | router.use('/users', require('./user/routes')) 10 | 11 | router.post('/test', (req, res) => { 12 | res.status(200).json(req.headers) 13 | }) 14 | 15 | // Return router 16 | module.exports = router 17 | -------------------------------------------------------------------------------- /modules/v1/setup/controller.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const actionsPath = './actions/' 3 | const extend = require('extend') 4 | const Model = require('./model').model 5 | 6 | const controllerActions = {} 7 | 8 | // Import default actions 9 | const importActions = ['find', 'findById', 'remove'] 10 | const createMethods = (element, index) => { 11 | controllerActions[element] = rfr(actionsPath + element)(Model) 12 | } 13 | importActions.forEach(createMethods) 14 | 15 | const arrayToObject = (array) => { 16 | return array.reduce((obj, item) => { 17 | obj[item.key] = item.value 18 | return obj 19 | }, {}) 20 | } 21 | 22 | // Controller custom actions 23 | const customMethods = { 24 | /** 25 | * Searches and fetches the resource in database 26 | * @method find 27 | * @param {Request} req Express Request Object 28 | * @param {Response} res Express Response Object 29 | * @return {Boolean} Wheter the response was sent or not 30 | */ 31 | find: (req, res) => { 32 | Model 33 | .findOne({}) 34 | .exec((err, data) => { 35 | if (err || !(data)) throw err 36 | let payload = data.data 37 | 38 | if (!req.query.full) { 39 | payload = arrayToObject(data.data) 40 | } 41 | 42 | res.status(200).json(payload) 43 | }) 44 | }, 45 | create: (req, res) => { 46 | const mod = req.body 47 | 48 | Model 49 | .findOne({}) 50 | .exec((err, modelInstance) => { 51 | if (err) throw err 52 | 53 | modelInstance.data.push(mod) 54 | modelInstance.save((error, document) => { 55 | if (error) throw error 56 | 57 | const payload = document.data.find(s => s.key === mod.key) 58 | res.status(201).json(payload) 59 | }) 60 | }) 61 | }, 62 | update: (req, res) => { 63 | console.log(`Chegou no update`) 64 | const mod = req.body 65 | 66 | Model.findOneAndUpdate({}, {$set: mod}, {new: true}, (err, modelInstance) => { 67 | if (err) throw err 68 | if (!modelInstance) res.status(404).json({error: 'setup_not_found'}) 69 | 70 | modelInstance 71 | .populate('last_updated_by', (err, setup) => { 72 | if (err) throw err 73 | res.status(200).json(setup) 74 | }) 75 | }) 76 | }, 77 | checkExists: (req, res) => { 78 | Model 79 | .count({data: {$elemMatch: req.body}}) 80 | .exec((err, value) => { 81 | if (err) throw err 82 | res.status(200).json({exists: !!(value >= 1)}) 83 | }) 84 | } 85 | } 86 | 87 | extend(controllerActions, customMethods) 88 | module.exports = controllerActions 89 | -------------------------------------------------------------------------------- /modules/v1/setup/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const modelName = 'setup' 3 | 4 | const structure = { 5 | data: [{ 6 | label: { 7 | required: true, 8 | type: String 9 | }, 10 | key: { 11 | unique: true, 12 | required: true, 13 | type: String 14 | }, 15 | value: { 16 | required: true, 17 | type: String 18 | } 19 | }] 20 | } 21 | 22 | const options = { 23 | strict: false, 24 | timestamps: { 25 | createdAt: 'created_at', 26 | updatedAt: 'updated_at' 27 | } 28 | } 29 | 30 | const schema = mongoose.Schema(structure, options) 31 | const model = mongoose.model(modelName, schema) 32 | 33 | module.exports = { 34 | schema: schema, 35 | model: model 36 | } 37 | -------------------------------------------------------------------------------- /modules/v1/setup/routes.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const router = require('express').Router() 3 | const controller = require('./controller') 4 | const validators = require('./validators') 5 | 6 | const jwtMiddleware = rfr('/helpers/jwt').middleware 7 | 8 | // Get 9 | router.get('/', controller.find) 10 | 11 | // Create 12 | router.post('/', [jwtMiddleware, validators.create, validators.uniqueKeyValidator], controller.create) 13 | 14 | // Update 15 | router.put('/', [jwtMiddleware], controller.update) 16 | 17 | // Check if exists 18 | router.post('/key', [jwtMiddleware, validators.keyCheck], controller.checkExists) 19 | 20 | module.exports = router 21 | -------------------------------------------------------------------------------- /modules/v1/setup/validators.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const handleValidation = rfr('/helpers/validation') 3 | const Model = require('./model').model 4 | 5 | const uniqueKeyValidator = (req, res, next) => { 6 | Model 7 | .findOne({data: {$elemMatch: {key: req.body.key}}}) 8 | .exec((err, modelInstance) => { 9 | if (err) throw err 10 | 11 | if (!modelInstance) { 12 | next() 13 | } else { 14 | const errorMessage = { 15 | 'key': { 16 | param: 'key', 17 | msg: { 18 | error: 'unique' 19 | } 20 | } 21 | } 22 | res.status(409).json(errorMessage) 23 | return false 24 | } 25 | }) 26 | } 27 | 28 | const labelValidators = (req) => { 29 | req.checkBody('label', {error: 'required'}).notEmpty() 30 | req.checkBody('label', {error: 'length', min: 3, max: 350}).len(3, 350) 31 | } 32 | const keyValidators = (req) => { 33 | req.checkBody('key', {error: 'required'}).notEmpty() 34 | req.checkBody('key', {error: 'length', min: 3, max: 350}).len(3, 350) 35 | } 36 | const valueValidators = (req) => { 37 | req.checkBody('value', {error: 'required'}).notEmpty() 38 | req.checkBody('value', {error: 'length', min: 3, max: 350}).len(3, 350) 39 | } 40 | const setupValidator = (req) => { 41 | labelValidators(req) 42 | keyValidators(req) 43 | valueValidators(req) 44 | } 45 | 46 | module.exports = { 47 | create: (req, res, next) => { 48 | setupValidator(req) 49 | handleValidation(req, res, next) 50 | }, 51 | update: (req, res, next) => { 52 | // req.checkBody('name', {error: 'length', min: 4, max: 20}).len(4, 20) 53 | // req.checkBody('email', {error: 'invalid'}).isEmail() 54 | }, 55 | keyCheck: (req, res, next) => { 56 | keyValidators(req) 57 | handleValidation(req, res, next) 58 | }, 59 | uniqueKeyValidator 60 | } 61 | -------------------------------------------------------------------------------- /modules/v1/user/controller.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const actionsPath = './actions/' 3 | const Model = require('./model').model 4 | const extend = require('extend') 5 | const createQueryObject = rfr('helpers/request').createQueryObject 6 | 7 | const controllerActions = {} 8 | 9 | // Import default actions 10 | const importActions = ['create', 'findById', 'findOneAndUpdate', 'update', 'remove', 'checkExists'] 11 | const createMethods = (element, index) => { 12 | controllerActions[element] = rfr(actionsPath + element)(Model) 13 | } 14 | importActions.forEach(createMethods) 15 | 16 | // Controller custom actions 17 | const customMethods = { 18 | find: (req, res) => { 19 | let query = createQueryObject(req) 20 | 21 | if (req.query.filter && req.query.filter.length) { 22 | let regex = new RegExp(req.query.filter, 'i') 23 | query = Object.assign(query, { 24 | '$or': [ 25 | {name: regex}, 26 | {username: regex}, 27 | {email: regex} 28 | ] 29 | }) 30 | } 31 | 32 | const pagOptions = { 33 | page: (Number.parseInt(req.query.page) - 1) || 0, 34 | limit: Number.parseInt(req.query.limit) || 15, 35 | sort: req.query.sort || {'name': 'asc'} 36 | } 37 | 38 | Model 39 | .find(query) 40 | .count() 41 | .exec((err, count) => { 42 | if (err) throw err 43 | const meta = { 44 | currentPage: (pagOptions.page + 1), 45 | limit: pagOptions.limit, 46 | totalPages: Math.ceil(count / pagOptions.limit), 47 | count: count 48 | } 49 | Model 50 | .find(query) 51 | .sort(pagOptions.sort) 52 | .skip(pagOptions.page * pagOptions.limit) 53 | .limit(pagOptions.limit) 54 | .exec((err, data) => { 55 | if (err) throw err 56 | const response = { 57 | users: data, 58 | meta: meta 59 | } 60 | res.status(200).json(response) 61 | }) 62 | }) 63 | } 64 | } 65 | 66 | extend(controllerActions, customMethods) 67 | module.exports = controllerActions 68 | -------------------------------------------------------------------------------- /modules/v1/user/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const modelName = 'user' 3 | 4 | const structure = { 5 | name: { 6 | type: String, 7 | required: true 8 | }, 9 | username: { 10 | type: String, 11 | required: true, 12 | unique: true 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | unique: true 18 | }, 19 | password: { 20 | type: String, 21 | required: true, 22 | bcrypt: true, 23 | select: false 24 | }, 25 | permissions: [{ 26 | label: { 27 | required: true, 28 | type: String 29 | }, 30 | resource: { 31 | required: true, 32 | type: String 33 | }, 34 | value: [{ 35 | type: String 36 | }] 37 | }], 38 | passwordToken: { 39 | type: String, 40 | required: false, 41 | select: false 42 | }, 43 | active: { 44 | type: Boolean, 45 | required: true, 46 | default: true 47 | } 48 | } 49 | 50 | const options = { 51 | timestamps: { 52 | createdAt: 'created_at', 53 | updatedAt: 'updated_at' 54 | } 55 | } 56 | 57 | const schema = mongoose.Schema(structure, options) 58 | schema.plugin(require('mongoose-bcrypt'), {rounds: 10}) 59 | 60 | const model = mongoose.model(modelName, schema) 61 | 62 | module.exports = { 63 | schema: schema, 64 | model: model 65 | } 66 | -------------------------------------------------------------------------------- /modules/v1/user/routes.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const router = require('express').Router() 3 | const controller = require('./controller') 4 | const validators = require('./validators') 5 | const jwtMiddleware = rfr('/helpers/jwt').middleware 6 | 7 | // Create 8 | router.post('/', [jwtMiddleware, validators.create, validators.uniqueEmailValidator, validators.uniqueUsernameValidator], controller.create) 9 | 10 | // Get 11 | router.get('/', [jwtMiddleware], controller.find) 12 | 13 | // Check if exists 14 | router.post('/email', [validators.email], controller.checkExists) 15 | router.post('/username', [validators.username], controller.checkExists) 16 | 17 | // Get by Id 18 | router.get('/:id', [], controller.findById) 19 | 20 | // Update 21 | router.patch('/:id', [jwtMiddleware, validators.update, validators.uniqueEmailValidator, validators.uniqueUsernameValidator], controller.findOneAndUpdate) 22 | 23 | // Delete 24 | router.delete('/:id', [jwtMiddleware], controller.remove) 25 | 26 | module.exports = router 27 | -------------------------------------------------------------------------------- /modules/v1/user/validators.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const handleValidation = rfr('/helpers/validation') 3 | const Model = require('./model').model 4 | 5 | const uniqueEmailValidator = (req, res, next) => { 6 | Model 7 | .findOne({email: req.body.email}) 8 | .exec((err, value) => { 9 | if (err) throw err 10 | 11 | if (!value) return next() 12 | if (req.params.id && value._id.equals(req.params.id)) return next() 13 | const errorMessage = { 14 | email: {param: 'email', msg: {error: 'unique'}} 15 | } 16 | res.status(409).json(errorMessage) 17 | return false 18 | }) 19 | } 20 | 21 | const uniqueUsernameValidator = (req, res, next) => { 22 | Model 23 | .findOne({username: req.body.username}) 24 | .exec((err, value) => { 25 | if (err) throw err 26 | 27 | if (!value) return next() 28 | if (req.params.id && value._id.equals(req.params.id)) return next() 29 | const errorMessage = { 30 | username: {param: 'username', msg: {error: 'unique'}} 31 | } 32 | res.status(409).json(errorMessage) 33 | return false 34 | }) 35 | } 36 | 37 | const nameValidators = (req) => { 38 | req.checkBody('name', {error: 'required'}).notEmpty() 39 | req.checkBody('name', {error: 'length', min: 4, max: 20}).len(4, 20) 40 | } 41 | const emailValidators = (req) => { 42 | req.checkBody('email', {error: 'required'}).notEmpty() 43 | req.checkBody('email', {error: 'invalid'}).isEmail() 44 | } 45 | const usernameValidators = (req) => { 46 | req.checkBody('username', {error: 'required'}).notEmpty() 47 | req.checkBody('username', {error: 'length', min: 4, max: 20}).len(4, 20) 48 | } 49 | const passwordValidators = (req) => { 50 | req.checkBody('password', {error: 'required'}).notEmpty() 51 | req.checkBody('password', {error: 'length', min: 6, max: 20}).len(6, 20) 52 | } 53 | const activeValidators = (req) => { 54 | req.checkBody('active', {error: 'required'}).notEmpty() 55 | req.checkBody('active', {error: 'invalid'}).isBoolean() 56 | } 57 | 58 | const userValidator = (req) => { 59 | nameValidators(req) 60 | passwordValidators(req) 61 | activeValidators(req) 62 | emailValidators(req) 63 | usernameValidators(req) 64 | } 65 | 66 | module.exports = { 67 | create: (req, res, next) => { 68 | userValidator(req) 69 | handleValidation(req, res, next) 70 | }, 71 | replace: (req, res, next) => { 72 | userValidator(req) 73 | handleValidation(req, res, next) 74 | }, 75 | update: (req, res, next) => { 76 | if (req.body && req.body.name) req.checkBody('name', {error: 'length', min: 4, max: 20}).len(4, 20) 77 | if (req.body && req.body.email) req.checkBody('email', {error: 'invalid'}).isEmail() 78 | if (req.body && req.body.username) req.checkBody('username', {error: 'length', min: 4, max: 20}).len(4, 20) 79 | if (req.body && req.body.password) req.checkBody('password', {error: 'length', min: 6, max: 20}).len(6, 20) 80 | if (req.body && req.body.active) req.checkBody('active', {error: 'invalid'}).isBoolean() 81 | handleValidation(req, res, next) 82 | }, 83 | email (req, res, next) { 84 | emailValidators(req) 85 | handleValidation(req, res, next) 86 | }, 87 | username (req, res, next) { 88 | usernameValidators(req) 89 | handleValidation(req, res, next) 90 | }, 91 | uniqueEmailValidator, 92 | uniqueUsernameValidator 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-rest-api", 3 | "version": "1.1.0", 4 | "description": "Express REST API", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "yarn run lint && yarn run test:unit && yarn run test:integration", 8 | "start": "nodemon index.js", 9 | "dev": "NODE_ENV=development nodemon index.js", 10 | "dev-win": "NODE_ENV=development nodemon -L index.js", 11 | "lint": "./node_modules/.bin/eslint . --ext .js", 12 | "prod": "NODE_ENV=production nodemon index.js", 13 | "seed": "node seeds/index.js", 14 | "test:integration": "mocha tests/integration/**/*.spec.js --exit", 15 | "test:integration-server": "mocha tests/integration/**/*.spec.js --watch", 16 | "test:unit": "mocha tests/unit/**/*.spec.js --exit", 17 | "test:unit-server": "mocha tests/unit/**/*.spec.js --watch", 18 | "test:report": "nyc --reporter=html --reporter=text mocha tests/**/*.spec.js" 19 | }, 20 | "keywords": [ 21 | "express", 22 | "es6", 23 | "tdd", 24 | "api", 25 | "restful", 26 | "mongoose" 27 | ], 28 | "author": "gfviegas", 29 | "dependencies": { 30 | "bcrypt": "^1.0.3", 31 | "body-parser": "1.18.2", 32 | "busboy": "^0.2.14", 33 | "bytes": "3.0.0", 34 | "concat-stream": "^1.4.6", 35 | "cors": "^2.8.4", 36 | "debug": "3.1.0", 37 | "dotenv": "5.0.0", 38 | "emailjs": "2.0.0", 39 | "express": "4.16.2", 40 | "express-validator": "5.0.1", 41 | "extend": "^3.0.0", 42 | "fb": "2.0.0", 43 | "jsonwebtoken": "8.1.1", 44 | "moment": "2.20.1", 45 | "mongo-dot-notation": "^1.2.0", 46 | "mongoose": "5.0.6", 47 | "mongoose-bcrypt": "^1.4.2", 48 | "morgan": "1.9.0", 49 | "multer": "^1.3.0", 50 | "pug": "2.0.0-rc.4", 51 | "request": "2.83.0", 52 | "request-promise": "4.2.2", 53 | "rfr": "^1.2.3", 54 | "util": "^0.10.3", 55 | "validator": "9.4.1" 56 | }, 57 | "devDependencies": { 58 | "chai": "4.1.2", 59 | "chai-shallow-deep-equal": "^1.4.6", 60 | "chalk": "2.3.1", 61 | "dirty-chai": "^2.0.1", 62 | "eslint": "^4.18.1", 63 | "eslint-config-default": "^0.2.1", 64 | "eslint-config-standard": "^11.0.0", 65 | "eslint-plugin-import": "^2.9.0", 66 | "eslint-plugin-node": "^6.0.0", 67 | "eslint-plugin-promise": "^3.6.0", 68 | "eslint-plugin-standard": "^3.0.1", 69 | "istanbul": "^0.4.5", 70 | "mocha": "5.0.1", 71 | "nodemon": "1.15.1", 72 | "nyc": "11.4.1", 73 | "sinon": "4.3.0", 74 | "sinon-mongoose": "2.0.2", 75 | "standard": "11.0.0", 76 | "supertest": "3.0.0" 77 | }, 78 | "standard": { 79 | "globals": [ 80 | "describe", 81 | "it", 82 | "expect", 83 | "beforeEach", 84 | "afterEach" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /public/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfviegas/express-rest-api/186048efc5baa70c252deb695aa7be5e1c726ab3/public/assets/img/logo.png -------------------------------------------------------------------------------- /public/assets/img/no_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfviegas/express-rest-api/186048efc5baa70c252deb695aa7be5e1c726ab3/public/assets/img/no_image.png -------------------------------------------------------------------------------- /public/assets/img/not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfviegas/express-rest-api/186048efc5baa70c252deb695aa7be5e1c726ab3/public/assets/img/not_found.png -------------------------------------------------------------------------------- /public/assets/img/reset-password.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfviegas/express-rest-api/186048efc5baa70c252deb695aa7be5e1c726ab3/public/assets/img/reset-password.jpg -------------------------------------------------------------------------------- /public/assets/img/updated-password.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfviegas/express-rest-api/186048efc5baa70c252deb695aa7be5e1c726ab3/public/assets/img/updated-password.jpg -------------------------------------------------------------------------------- /public/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfviegas/express-rest-api/186048efc5baa70c252deb695aa7be5e1c726ab3/public/files/.gitkeep -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: * 3 | -------------------------------------------------------------------------------- /seeds/config/index.js: -------------------------------------------------------------------------------- 1 | const data = {} 2 | 3 | module.exports = data 4 | -------------------------------------------------------------------------------- /seeds/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const rfr = require('rfr') 3 | const seeder = require('./seeder') 4 | 5 | const chalk = require('chalk') 6 | const error = chalk.bold.red 7 | const success = chalk.bold.green 8 | const info = chalk.bold.blue 9 | 10 | const seedSetup = () => { 11 | console.log(info('\n**** Seeding Setup... ****\n')) 12 | const model = rfr('modules/v1/setup/model').model 13 | const data = require('./setup') 14 | 15 | return seeder.clearAndSeed(model, data) 16 | } 17 | 18 | const seedUsers = () => { 19 | console.log(info('\n**** Seeding Users... ****\n')) 20 | const model = rfr('modules/v1/user/model').model 21 | const data = require('./user') 22 | 23 | return seeder.clearAndSeed(model, data) 24 | } 25 | 26 | const execute = () => { 27 | const funcs = [seedSetup(), seedUsers()] 28 | return Promise.all(funcs) 29 | } 30 | 31 | seeder.connect().then(() => { 32 | console.log(success('**** DB connected sucessfully **** \n')) 33 | execute() 34 | .then(response => { 35 | let count = 0 36 | response.forEach(() => count++) 37 | console.log(success(`\n ### All Seeds done! ### \n`)) 38 | console.log(success(`\n ### ${count} seeds ran! ### \n`)) 39 | process.exit(0) 40 | }) 41 | .catch(err => { 42 | console.log(error(err)) 43 | process.exit(1) 44 | }) 45 | }).catch((err) => { 46 | console.log(error(err)) 47 | process.exit(1) 48 | }) 49 | -------------------------------------------------------------------------------- /seeds/seeder.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | mongoose.Promise = global.Promise 3 | 4 | const connect = () => { 5 | return mongoose.connect(process.env.DB_HOST) 6 | } 7 | 8 | const clearModel = (Model, cb) => { 9 | return Model.remove(cb) 10 | } 11 | 12 | const clearAndSeed = (Model, data) => { 13 | return new Promise((resolve, reject) => { 14 | Model.remove(err => { 15 | if (err) throw err 16 | Model.create(data).then(data => resolve(data)).catch(data => reject(data)) 17 | }) 18 | }) 19 | } 20 | 21 | const seed = (Model, data) => { 22 | return Model.create(data) 23 | } 24 | 25 | module.exports = { 26 | connect, 27 | clearModel, 28 | clearAndSeed, 29 | seed 30 | } 31 | -------------------------------------------------------------------------------- /seeds/setup/index.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | data: [ 3 | { 4 | label: 'Email de Contato', 5 | key: 'email', 6 | value: 'contact@example.com' 7 | } 8 | ] 9 | } 10 | 11 | module.exports = data 12 | -------------------------------------------------------------------------------- /seeds/user/index.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | email: 'example@example.com', 4 | username: 'exampleuser', 5 | name: 'John Doe', 6 | password: 'secret', 7 | active: true, 8 | permissions: [ 9 | {'resource': 'user', 'label': 'Usuários', 'value': ['view', 'manage']}, 10 | {'resource': 'setup', 'label': 'Configurações', 'value': ['view', 'manage']} 11 | ] 12 | } 13 | ] 14 | 15 | module.exports = data 16 | -------------------------------------------------------------------------------- /templates/mails/auth/reset-password.pug: -------------------------------------------------------------------------------- 1 | extends ../base-layout.pug 2 | 3 | block append banner 4 | td(bgcolor='#ffffff') 5 | img.fluid(src=`${_email.imgPath}reset-password.jpg`, aria-hidden='true', width='680', height='', alt='alt_text', border='0', align='center', style='width: 100%; max-width: 680px; height: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') 6 | block append content 7 | td(bgcolor='#ffffff') 8 | table(role='presentation', aria-hidden='true', cellspacing='0', cellpadding='0', border='0', width='100%') 9 | tr 10 | td(style='padding: 40px; text-align: center; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') 11 | p Olá, #{name}. Você esqueceu sua senha da #[strong Área Administrativa do #{_email.base_name}]? 12 | p Para recuperar sua senha, basta clicar no botão embaixo para confirmar a solicitação! 13 | p Se você não solicitou a mudança de senha, ignore esse email. #[strong Nada] será alterado. 14 | br 15 | br 16 | table(role='presentation', aria-hidden='true', cellspacing='0', cellpadding='0', border='0', align='center', style='margin: auto') 17 | tr 18 | td.button-td(style='border-radius: 3px; background: darkcyan; text-align: center;') 19 | a.button-a(href=`${_email.admin_url}auth/update/${passwordToken}`, style='background: darkcyan; border: 15px solid darkcyan; font-family: sans-serif; font-size: 13px; line-height: 1.1; text-align: center; text-decoration: none; display: block; border-radius: 3px; font-weight: bold;') 20 | span.button-link(style='color:#ffffff;')     Confirmar Solicitação     21 | -------------------------------------------------------------------------------- /templates/mails/auth/updated-password.pug: -------------------------------------------------------------------------------- 1 | extends ../base-layout.pug 2 | 3 | block append banner 4 | td(bgcolor='#ffffff') 5 | img.fluid(src=`${_email.imgPath}updated-password.jpg`, aria-hidden='true', width='680', height='', alt='alt_text', border='0', align='center', style='width: 100%; max-width: 680px; height: auto; background: #dddddd; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') 6 | block append content 7 | td(bgcolor='#ffffff') 8 | table(role='presentation', aria-hidden='true', cellspacing='0', cellpadding='0', border='0', width='100%') 9 | tr 10 | td(style='padding: 40px; text-align: center; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') 11 | p Olá, #{name}. Você atualizou a sua senha da #[strong Área Administrativa do #{_email.base_name}]. 12 | p Estamos te notificando que a sua senha foi atualizada com sucesso. 13 | p Se você não efetuou a mudança de senha, redefina-a imediatamente #[a(href=_email.admin_url) neste link]. 14 | br 15 | -------------------------------------------------------------------------------- /templates/mails/base-layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | meta(charset='utf-8') 5 | meta(name='viewport', content='width=device-width') 6 | meta(http-equiv='X-UA-Compatible', content='IE=edge') 7 | meta(name='x-apple-disable-message-reformatting') 8 | title 9 | //if mso 10 | style. 11 | * { 12 | font-family: sans-serif !important; 13 | } 14 | // [if !mso] { 10 | if (mongoose.connection.name !== process.env.DB_HOST_TEST.split('/')[3]) { 11 | logger.error(`[CRITICAL] - Not running in test environment DB!`) 12 | logger.error(`${mongoose.connection.name} !== ${process.env.DB_HOST_TEST.split('/')[3]}`) 13 | logger.error(`${JSON.stringify(mongoose.connection.hosts)}`) 14 | 15 | return process.exit(1) 16 | } 17 | 18 | logger.info(`[INFO] - Dropping the DB...`) 19 | mongoose.connection.dropDatabase() 20 | logger.info(`[INFO] - DB Dropped! \n`) 21 | } 22 | 23 | before(async () => { 24 | const res = await dropDB() 25 | if (res) logger.info(res) 26 | }) 27 | 28 | module.exports = app 29 | -------------------------------------------------------------------------------- /tests/config/chai.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | 3 | module.exports = () => { 4 | chai.use(require('dirty-chai')) 5 | chai.use(require('chai-shallow-deep-equal')) 6 | 7 | chai.config.includeStack = true 8 | } 9 | -------------------------------------------------------------------------------- /tests/helpers/auth.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const jwtHelper = rfr('helpers/jwt') 3 | 4 | module.exports = (Model) => { 5 | const helper = {} 6 | 7 | helper.getAuthorizedUser = (query) => { 8 | return Model.findOne(query) 9 | } 10 | 11 | helper.getJWTFromUser = (user) => { 12 | return jwtHelper.generateToken(user) 13 | } 14 | 15 | helper.getJWTFromUserQuery = (query) => { 16 | return new Promise(async (resolve, reject) => { 17 | const user = await helper.getAuthorizedUser(query) 18 | return resolve(jwtHelper.generateToken(user)) 19 | }) 20 | } 21 | 22 | return helper 23 | } 24 | -------------------------------------------------------------------------------- /tests/helpers/request.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const req = { 3 | checkParams: () => {}, 4 | checkBody: () => {}, 5 | notEmpty: () => {}, 6 | len: () => {}, 7 | isEmail: () => {}, 8 | isMongoId: () => {}, 9 | isBoolean: () => {}, 10 | matches: () => {}, 11 | isIn: () => {}, 12 | getValidationResult: () => {} 13 | } 14 | const checkBody = (req) => { return sinon.stub(req, 'checkBody').returnsThis() } 15 | const checkParams = (req) => { return sinon.stub(req, 'checkParams').returnsThis() } 16 | const notEmpty = (req) => { return sinon.stub(req, 'notEmpty').returnsThis() } 17 | const len = (req) => { return sinon.stub(req, 'len').returnsThis() } 18 | const isEmail = (req) => { return sinon.stub(req, 'isEmail').returnsThis() } 19 | const isIn = (req) => { return sinon.stub(req, 'isIn').returnsThis() } 20 | const isMongoId = (req) => { return sinon.stub(req, 'isMongoId').returnsThis() } 21 | const isBoolean = (req) => { return sinon.stub(req, 'isBoolean').returnsThis() } 22 | const matches = (req) => { return sinon.stub(req, 'matches').returnsThis() } 23 | const getValidationResult = (req) => { 24 | return sinon.stub(req, 'getValidationResult').callsFake(() => { 25 | return new Promise((resolve, reject) => { 26 | resolve({ 27 | array: () => { 28 | return [] 29 | }, 30 | mapped: () => { 31 | return [] 32 | }, 33 | isEmpty: () => { 34 | return true 35 | }, 36 | next: () => {} 37 | }) 38 | }) 39 | }) 40 | } 41 | 42 | const res = { 43 | status: () => {}, 44 | json: () => {} 45 | } 46 | const status = sinon.stub(res, 'status').returnsThis() 47 | const json = sinon.stub(res, 'json').returnsThis() 48 | 49 | module.exports = { 50 | stubReq: () => { return req }, 51 | stubCheckBody: (req) => { return checkBody(req) }, 52 | stubCheckParams: (req) => { return checkParams(req) }, 53 | stubNotEmpty: (req) => { return notEmpty(req) }, 54 | stubLen: (req) => { return len(req) }, 55 | stubIsEmail: (req) => { return isEmail(req) }, 56 | stubIsMongoId: (req) => { return isMongoId(req) }, 57 | stubIsBoolean: (req) => { return isBoolean(req) }, 58 | stubMatches: (req) => { return matches(req) }, 59 | stubIsIn: (req) => { return isIn(req) }, 60 | stubGetValidationResult: (req) => { return getValidationResult(req) }, 61 | 62 | stubRes: (req) => { return res(req) }, 63 | stubStatus: (req) => { return status(req) }, 64 | stubJson: (req) => { return json(req) } 65 | } 66 | -------------------------------------------------------------------------------- /tests/helpers/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = (routes) => { 2 | const registredRoutes = { 3 | 'head': [], 4 | 'get': [], 5 | 'post': [], 6 | 'put': [], 7 | 'patch': [], 8 | 'delete': [] 9 | } 10 | 11 | routes.stack.forEach((r) => { 12 | if (r.route && r.route.path) { 13 | let routeMethods = Object.keys(r.route.methods) 14 | routeMethods.forEach((method) => { 15 | registredRoutes[method].push(r.route.path) 16 | }) 17 | } 18 | }) 19 | 20 | return { 21 | getRoutes: () => { 22 | return registredRoutes 23 | }, 24 | checkRoute: (method, path) => { 25 | return !!(registredRoutes[method].indexOf(path) > -1) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/integration/modules/v1/auth.spec.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | 3 | const request = require('supertest') 4 | const chai = require('chai') 5 | const sinon = require('sinon') 6 | const expect = chai.expect 7 | 8 | const app = rfr('tests/config/app') 9 | const mongoose = require('mongoose') 10 | const Model = mongoose.model('user') 11 | const auth = rfr('tests/helpers/auth')(Model) 12 | const jwtHelper = require('jsonwebtoken') 13 | 14 | const ENDPOINT = '/api/v1/auth' 15 | 16 | describe(ENDPOINT, () => { 17 | let someUser, someUserData, someUserToken, someUserHeader 18 | 19 | before(async () => { 20 | someUserData = {username: 'testAuth', name: 'test', email: 'testAuth@test.com', password: 'test', active: true} 21 | someUser = await Model.create(someUserData) 22 | someUserToken = auth.getJWTFromUser(someUser) 23 | someUserHeader = {authorization: `Bearer ${someUserToken}`} 24 | }) 25 | 26 | after(async () => { 27 | Model.remove({username: 'testAuth'}) 28 | }) 29 | 30 | describe('POST /', () => { 31 | let userCredentials 32 | 33 | before(async () => { 34 | userCredentials = {username: someUserData.username, password: someUserData.password} 35 | }) 36 | 37 | it('should validate required params', async () => { 38 | const expectedMessage = { 39 | password: {msg: {error: 'required'}}, 40 | username: {msg: {error: 'required'}} 41 | } 42 | 43 | const res = await request(app) 44 | .post(ENDPOINT) 45 | .send({}) 46 | 47 | expect(res.type).to.equal(`application/json`) 48 | expect(res.body).to.shallowDeepEqual(expectedMessage) 49 | expect(res.status).to.equal(422) 50 | }) 51 | 52 | it('should respond with success status and the JWT when valid data is sent', async () => { 53 | const res = await request(app) 54 | .post(ENDPOINT) 55 | .send(userCredentials) 56 | 57 | expect(res.type).to.equal(`application/json`) 58 | expect(res.body).to.be.an('object') 59 | expect(res.body).to.contain.all.keys(['token']) 60 | expect(res.status).to.equal(200) 61 | }) 62 | }) 63 | 64 | describe('GET /', () => { 65 | it('should not accept unauthenticated request', async () => { 66 | const expectedMessage = {error: 'no_token'} 67 | const res = await request(app) 68 | .get(ENDPOINT) 69 | .expect(401) 70 | 71 | expect(res.type).to.equal(`application/json`) 72 | expect(res.body).to.deep.equal(expectedMessage) 73 | }) 74 | 75 | it('should respond with success status', async () => { 76 | const res = await request(app) 77 | .get(ENDPOINT) 78 | .set(someUserHeader) 79 | .expect(200) 80 | 81 | expect(res.type).to.equal(`application/json`) 82 | }) 83 | 84 | it('should return valid data', async () => { 85 | const res = await request(app) 86 | .get(ENDPOINT) 87 | .set(someUserHeader) 88 | .expect(200) 89 | 90 | expect(res.type).to.equal(`application/json`) 91 | expect(res.body).to.be.an('object') 92 | expect(res.body).to.contain.all.keys(['_id', 'updated_at', 'created_at', 'name', 'email', 'username', 'active']) 93 | expect(res.body.username).to.equal(someUser.username) 94 | }) 95 | 96 | it('should validate if token is expired', async () => { 97 | const payload = {sub: someUser.id, data: {name: someUser.name}, iat: 1437018582} 98 | const expiredToken = jwtHelper.sign(payload, process.env.APP_SECRET, {expiresIn: 1}) 99 | const hackedHeader = {authorization: `Bearer ${expiredToken}`} 100 | 101 | const clock = sinon.useFakeTimers(1437018582) 102 | await clock.tick(1437018650000) 103 | 104 | const res = await request(app) 105 | .get(ENDPOINT) 106 | .set(hackedHeader) 107 | .expect(403) 108 | 109 | expect(res.type).to.equal(`application/json`) 110 | expect(res.body).to.be.an('object') 111 | expect(res.body).to.contain.all.keys(['error']) 112 | expect(res.body.error).to.equal('token_expired') 113 | }) 114 | 115 | it('should validate if token signature is invalid', async () => { 116 | const hackedHeader = {authorization: `${someUserHeader.authorization}Aa + aa`} 117 | const res = await request(app) 118 | .get(ENDPOINT) 119 | .set(hackedHeader) 120 | .expect(401) 121 | 122 | expect(res.type).to.equal(`application/json`) 123 | expect(res.body).to.be.an('object') 124 | expect(res.body).to.contain.all.keys(['error']) 125 | expect(res.body.error).to.equal('invalid_signature') 126 | }) 127 | 128 | it('should validate if token is malformed', async () => { 129 | const hackedHeader = {authorization: `Bearer ${someUserHeader.authorization}A`} 130 | const res = await request(app) 131 | .get(ENDPOINT) 132 | .set(hackedHeader) 133 | .expect(401) 134 | 135 | expect(res.type).to.equal(`application/json`) 136 | expect(res.body).to.be.an('object') 137 | expect(res.body).to.contain.all.keys(['error']) 138 | expect(res.body.error).to.equal('jwt_malformed') 139 | }) 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /tests/integration/modules/v1/user.spec.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | 3 | const request = require('supertest') 4 | const chai = require('chai') 5 | const expect = chai.expect 6 | 7 | const app = rfr('tests/config/app') 8 | const mongoose = require('mongoose') 9 | const Model = mongoose.model('user') 10 | const auth = rfr('tests/helpers/auth')(Model) 11 | 12 | const ENDPOINT = '/api/v1/users' 13 | 14 | const testEntityValue = (entity, value) => { 15 | expect(entity).to.be.an('object') 16 | expect(entity).to.contain.all.keys(['_id', 'updated_at', 'created_at', 'name', 'email', 'username', 'active']) 17 | 18 | if (value && value.username) expect(entity.username).to.equal(value.username) 19 | } 20 | 21 | describe(ENDPOINT, () => { 22 | let authorizedUserToken, authorizedHeader, authorizedUser 23 | 24 | before(async () => { 25 | authorizedUser = await Model.create({username: 'test0', name: 'test', email: 'test0@test.com', password: 'test', active: true}) 26 | authorizedUserToken = auth.getJWTFromUser(authorizedUser) 27 | authorizedHeader = {authorization: `Bearer ${authorizedUserToken}`} 28 | }) 29 | 30 | after(async () => { 31 | Model.remove({username: 'test0'}) 32 | }) 33 | 34 | describe('POST /', () => { 35 | it('should not accept unauthenticated request', async () => { 36 | const expectedMessage = {error: 'no_token'} 37 | const res = await request(app) 38 | .post(ENDPOINT) 39 | .send({ }) 40 | 41 | expect(res.type).to.equal(`application/json`) 42 | expect(res.body).to.deep.equal(expectedMessage) 43 | expect(res.status).to.equal(401) 44 | }) 45 | 46 | it('should validate required params', async () => { 47 | const expectedMessage = { 48 | active: {msg: {error: 'required'}}, 49 | name: {msg: {error: 'required'}}, 50 | email: {msg: {error: 'required'}}, 51 | password: {msg: {error: 'required'}}, 52 | username: {msg: {error: 'required'}} 53 | } 54 | 55 | const res = await request(app) 56 | .post(ENDPOINT) 57 | .set(authorizedHeader) 58 | .send({ }) 59 | 60 | expect(res.type).to.equal(`application/json`) 61 | expect(res.body).to.shallowDeepEqual(expectedMessage) 62 | expect(res.status).to.equal(422) 63 | }) 64 | 65 | it('should validate invalid params', async () => { 66 | const expectedMessage = { 67 | email: {msg: {error: 'invalid'}}, 68 | active: {msg: {error: 'invalid'}}, 69 | name: {msg: {error: 'length', max: 20, min: 4}}, 70 | password: {msg: {error: 'length', max: 20, min: 6}}, 71 | username: {msg: {error: 'length', max: 20, min: 4}} 72 | } 73 | const userData = {email: '2--01', password: 'abcde', username: 'abc', active: 'NOT A BOOLEAN', name: 'asd'} 74 | 75 | const res = await request(app) 76 | .post(ENDPOINT) 77 | .set(authorizedHeader) 78 | .send(userData) 79 | 80 | expect(res.type).to.equal(`application/json`) 81 | expect(res.body).to.shallowDeepEqual(expectedMessage) 82 | expect(res.status).to.equal(422) 83 | }) 84 | 85 | it('should validate username uniqueness', async () => { 86 | const expectedMessage = {username: {msg: {error: 'unique'}}} 87 | const userData = {email: 'tes201@test.com', password: '111abcde', username: 'test0', active: true, name: 'new user'} 88 | 89 | const res = await request(app) 90 | .post(ENDPOINT) 91 | .set(authorizedHeader) 92 | .send(userData) 93 | 94 | expect(res.type).to.equal(`application/json`) 95 | expect(res.body).to.shallowDeepEqual(expectedMessage) 96 | expect(res.status).to.equal(409) 97 | }) 98 | 99 | it('should validate email uniqueness', async () => { 100 | const expectedMessage = {email: {msg: {error: 'unique'}}} 101 | const userData = {email: 'test0@test.com', password: '111abcde', username: 'johntravolta', active: true, name: 'new user'} 102 | 103 | const res = await request(app) 104 | .post(ENDPOINT) 105 | .set(authorizedHeader) 106 | .send(userData) 107 | 108 | expect(res.type).to.equal(`application/json`) 109 | expect(res.body).to.shallowDeepEqual(expectedMessage) 110 | expect(res.status).to.equal(409) 111 | }) 112 | 113 | it('should respond with success status and the created user when valid data is sent', async () => { 114 | const userData = {email: 'testing0@gmail.com', password: 'potato', username: 'potatouser0', active: true, name: 'test batata'} 115 | const res = await request(app) 116 | .post(ENDPOINT) 117 | .set(authorizedHeader) 118 | .send(userData) 119 | 120 | expect(res.type).to.equal(`application/json`) 121 | testEntityValue(res.body, userData) 122 | expect(res.status).to.equal(201) 123 | }) 124 | 125 | it('should create a new record on DB when valid data is sent', async () => { 126 | const userCount = await Model.find({}).count().exec() 127 | const userData = {email: 'testing1@gmail.com', password: 'potato', username: 'potatouser1', active: true, name: 'batata test'} 128 | 129 | const res = await request(app) 130 | .post(ENDPOINT) 131 | .set(authorizedHeader) 132 | .send(userData) 133 | 134 | const newUserCount = await Model.find({}).count().exec() 135 | expect(newUserCount).to.equal(userCount + 1) 136 | const newUserFind = await Model.find({username: res.username}).count().exec() 137 | expect(newUserFind._id).to.equal(res._id) 138 | }) 139 | }) 140 | 141 | describe('GET /', () => { 142 | it('should not accept unauthenticated request', async () => { 143 | const expectedMessage = {error: 'no_token'} 144 | const res = await request(app) 145 | .get(ENDPOINT) 146 | .expect(401) 147 | 148 | expect(res.type).to.equal(`application/json`) 149 | expect(res.body).to.deep.equal(expectedMessage) 150 | }) 151 | 152 | it('should respond with success status', async () => { 153 | const res = await request(app) 154 | .get(ENDPOINT) 155 | .set(authorizedHeader) 156 | .expect(200) 157 | 158 | expect(res.type).to.equal(`application/json`) 159 | }) 160 | 161 | it('should return valid data', async () => { 162 | const res = await request(app) 163 | .get(ENDPOINT) 164 | .set(authorizedHeader) 165 | .expect(200) 166 | 167 | expect(res.type).to.equal(`application/json`) 168 | expect(res.body).to.contain.all.keys(['users', 'meta']) 169 | expect(res.body.meta).to.be.an('object') 170 | expect(res.body.meta).to.contain.all.keys(['currentPage', 'limit', 'totalPages', 'count']) 171 | expect(res.body.users).to.be.an('array') 172 | res.body.users.every(u => { 173 | expect(u).to.be.an('object') 174 | expect(u).to.contain.all.keys(['permissions', 'active', '_id', 'email', 'username', 'name', 'created_at', 'updated_at']) 175 | return true 176 | }) 177 | }) 178 | }) 179 | 180 | describe('GET /:id', () => { 181 | it('should not accept unauthenticated request', async () => { 182 | const expectedMessage = {error: 'no_token'} 183 | const res = await request(app) 184 | .get(ENDPOINT) 185 | .expect(401) 186 | 187 | expect(res.type).to.equal(`application/json`) 188 | expect(res.body).to.deep.equal(expectedMessage) 189 | }) 190 | 191 | it('should respond a 404 when not found', async () => { 192 | const res = await request(app) 193 | .get(`${ENDPOINT}/585359000214ed0c00fa00a0`) 194 | .set(authorizedHeader) 195 | .expect(404) 196 | 197 | expect(res.type).to.equal(`application/json`) 198 | }) 199 | 200 | it('should respond a valid found entity', async () => { 201 | const res = await request(app) 202 | .get(`${ENDPOINT}/${authorizedUser._id}`) 203 | .set(authorizedHeader) 204 | .expect(200) 205 | 206 | expect(res.type).to.equal(`application/json`) 207 | expect(res.body).to.be.an('object') 208 | expect(res.body).to.contain.all.keys(['permissions', 'active', '_id', 'email', 'username', 'name', 'created_at', 'updated_at']) 209 | }) 210 | }) 211 | 212 | describe('PATCH /:id', () => { 213 | let someNewUser 214 | 215 | before(async () => { 216 | someNewUser = await Model.create({username: 'toupdate', name: 'updated', email: 'test0@update.com', password: 'update', active: true}) 217 | }) 218 | 219 | it('should not accept unauthenticated request', async () => { 220 | const expectedMessage = {error: 'no_token'} 221 | const res = await request(app) 222 | .patch(`${ENDPOINT}/${authorizedUser._id}`, {}) 223 | .expect(401) 224 | 225 | expect(res.type).to.equal(`application/json`) 226 | expect(res.body).to.deep.equal(expectedMessage) 227 | }) 228 | 229 | it('should respond a 404 when not found', async () => { 230 | const res = await request(app) 231 | .patch(`${ENDPOINT}/585359000214ed0c00000000`) 232 | .set(authorizedHeader) 233 | .send({username: 'abasasasc'}) 234 | 235 | expect(res.type).to.equal(`application/json`) 236 | expect(res.status).to.equal(404) 237 | }) 238 | 239 | it('should validate invalid params', async () => { 240 | const expectedMessage = { 241 | email: {msg: {error: 'invalid'}}, 242 | active: {msg: {error: 'invalid'}}, 243 | name: {msg: {error: 'length', max: 20, min: 4}}, 244 | username: {msg: {error: 'length', max: 20, min: 4}} 245 | } 246 | const userData = {email: '2--01', username: 'abc', active: 'NOT A BOOLEAN', name: 'asd'} 247 | 248 | const res = await request(app) 249 | .patch(`${ENDPOINT}/${authorizedUser._id}`) 250 | .set(authorizedHeader) 251 | .send(userData) 252 | 253 | expect(res.type).to.equal(`application/json`) 254 | expect(res.body).to.shallowDeepEqual(expectedMessage) 255 | expect(res.status).to.equal(422) 256 | }) 257 | 258 | it('should validate username uniqueness', async () => { 259 | const expectedMessage = {username: {msg: {error: 'unique'}}} 260 | const userData = {username: 'test0'} 261 | 262 | const res = await request(app) 263 | .patch(`${ENDPOINT}/${someNewUser._id}`) 264 | .set(authorizedHeader) 265 | .send(userData) 266 | 267 | expect(res.type).to.equal(`application/json`) 268 | expect(res.body).to.shallowDeepEqual(expectedMessage) 269 | expect(res.status).to.equal(409) 270 | }) 271 | 272 | it('should validate email uniqueness', async () => { 273 | const expectedMessage = {email: {msg: {error: 'unique'}}} 274 | const userData = {email: 'test0@test.com'} 275 | 276 | const res = await request(app) 277 | .patch(`${ENDPOINT}/${someNewUser._id}`) 278 | .set(authorizedHeader) 279 | .send(userData) 280 | 281 | expect(res.type).to.equal(`application/json`) 282 | expect(res.body).to.shallowDeepEqual(expectedMessage) 283 | expect(res.status).to.equal(409) 284 | }) 285 | 286 | it('should respond with success status and the created user when valid data is sent', async () => { 287 | const userData = {username: 'potatouser222', name: 'TEST'} 288 | const res = await request(app) 289 | .patch(`${ENDPOINT}/${authorizedUser._id}`) 290 | .set(authorizedHeader) 291 | .send(userData) 292 | 293 | expect(res.type).to.equal(`application/json`) 294 | testEntityValue(res.body, userData) 295 | expect(res.status).to.equal(200) 296 | }) 297 | 298 | it('should update the matching record on DB when valid data is sent', async () => { 299 | const oldUserData = await Model.findById(authorizedUser._id).exec() 300 | const userData = {active: false, name: 'batata test'} 301 | 302 | const res = await request(app) 303 | .patch(`${ENDPOINT}/${authorizedUser._id}`) 304 | .set(authorizedHeader) 305 | .send(userData) 306 | 307 | expect(res.body.active).to.not.equal(oldUserData.active) 308 | expect(res.body._id).to.equal(res.body._id) 309 | }) 310 | }) 311 | 312 | describe('DELETE /:id', () => { 313 | it('should not accept unauthenticated request', async () => { 314 | const expectedMessage = {error: 'no_token'} 315 | const res = await request(app) 316 | .delete(`${ENDPOINT}/${authorizedUser._id}`, {}) 317 | .expect(401) 318 | 319 | expect(res.body).to.deep.equal(expectedMessage) 320 | }) 321 | 322 | it('should respond a 404 when not found', async () => { 323 | const res = await request(app) 324 | .delete(`${ENDPOINT}/585359000214ed0c00000000`) 325 | .set(authorizedHeader) 326 | .expect(404) 327 | 328 | expect(res.status).to.equal(404) 329 | }) 330 | 331 | it('should respond with success status and the created user when valid data is sent', async () => { 332 | const toDeleteUser = await Model.create({username: 'todelete', name: 'will be deleted', email: 'test0@delete.com', password: 'delete', active: true}) 333 | const res = await request(app) 334 | .delete(`${ENDPOINT}/${toDeleteUser._id}`) 335 | .set(authorizedHeader) 336 | .expect(204) 337 | 338 | expect(res.status).to.equal(204) 339 | }) 340 | 341 | it('should remove the matching record on DB when authorized request', async () => { 342 | const toDeleteUser = await Model.create({username: 'todelete1', name: 'will be deleted', email: 'tes1t0@delete.com', password: 'delete', active: true}) 343 | await request(app) 344 | .delete(`${ENDPOINT}/${toDeleteUser._id}`) 345 | .set(authorizedHeader) 346 | .expect(204) 347 | 348 | const queryingUser = await Model.find({_id: toDeleteUser._id}).count() 349 | expect(queryingUser).to.equal(0) 350 | }) 351 | }) 352 | }) 353 | -------------------------------------------------------------------------------- /tests/unit/modules/v1/routes.spec.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const chai = require('chai') 3 | const expect = chai.expect 4 | 5 | const moduleRoutes = rfr('./modules/v1/routes') 6 | const routesHelper = rfr('./tests/helpers/routes')(moduleRoutes) 7 | const routes = routesHelper.getRoutes() 8 | 9 | describe('Modules V1: Routes', () => { 10 | describe('HEAD', () => { 11 | it('should not have any HEAD routes', () => { 12 | expect(routes.head.length).to.equal(0) 13 | }) 14 | }) 15 | describe('GET', () => { 16 | it('should not have any GET routes', () => { 17 | expect(routes.get.length).to.equal(0) 18 | }) 19 | }) 20 | describe('POST', () => { 21 | it('should have a POST /test routes', () => { 22 | expect(routesHelper.checkRoute('post', '/test')).to.equal(true) 23 | }) 24 | }) 25 | describe('PUT', () => { 26 | it('should not have any PUT routes', () => { 27 | expect(routes.put.length).to.equal(0) 28 | }) 29 | }) 30 | describe('PATCH', () => { 31 | it('should not have any PATCH routes', () => { 32 | expect(routes.patch.length).to.equal(0) 33 | }) 34 | }) 35 | describe('DELETE', () => { 36 | it('should not have any DELETE routes', () => { 37 | expect(routes.delete.length).to.equal(0) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/unit/modules/v1/user/controller.spec.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const chai = require('chai') 3 | const expect = chai.expect 4 | 5 | const controller = rfr('./modules/v1/user/controller') 6 | 7 | describe('Module User: Controller', () => { 8 | it('should have all routes required methods registred', () => { 9 | expect(controller).to.contain.all.keys(['create', 'find', 'findById', 'findOneAndUpdate', 'update', 'remove', 'checkExists']) 10 | }) 11 | 12 | describe('Method checkExists', () => { 13 | it('should be a function', () => { 14 | expect(controller.checkExists).to.be.a('function') 15 | }) 16 | 17 | it('should send a valid response', () => { 18 | let res = { 19 | status: (code) => { 20 | return res 21 | }, 22 | json: (data) => { 23 | expect(data).to.be.a('object') 24 | expect(data).to.contain.all.keys(['tested']) 25 | expect(data.tested).to.equal(true) 26 | } 27 | } 28 | 29 | controller.checkExists({}, res) 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/unit/modules/v1/user/routes.spec.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const chai = require('chai') 3 | const expect = chai.expect 4 | 5 | const moduleRoutes = rfr('./modules/v1/user/routes') 6 | const routesHelper = rfr('./tests/helpers/routes')(moduleRoutes) 7 | const routes = routesHelper.getRoutes() 8 | 9 | describe('Module User: Routes', () => { 10 | describe('HEAD', () => { 11 | it('should not have any HEAD routes', () => { 12 | expect(routes.head.length).to.equal(0) 13 | }) 14 | }) 15 | describe('GET', () => { 16 | it('should have a GET / route', () => { 17 | expect(routesHelper.checkRoute('get', '/')).to.equal(true) 18 | }) 19 | it('should have a GET /:id route', () => { 20 | expect(routesHelper.checkRoute('get', '/:id')).to.equal(true) 21 | }) 22 | }) 23 | describe('POST', () => { 24 | it('should have a POST / route', () => { 25 | expect(routesHelper.checkRoute('post', '/')).to.equal(true) 26 | }) 27 | it('should have a POST /email route', () => { 28 | expect(routesHelper.checkRoute('post', '/email')).to.equal(true) 29 | }) 30 | }) 31 | describe('PUT', () => { 32 | it('should not have any PUT routes', () => { 33 | expect(routes.put.length).to.equal(0) 34 | }) 35 | }) 36 | describe('PATCH', () => { 37 | it('should have a PATCH /:id route', () => { 38 | expect(routesHelper.checkRoute('patch', '/:id')).to.equal(true) 39 | }) 40 | }) 41 | describe('DELETE', () => { 42 | it('should have a DELETE /:id route', () => { 43 | expect(routesHelper.checkRoute('delete', '/:id')).to.equal(true) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/unit/modules/v1/user/validators.spec.js: -------------------------------------------------------------------------------- 1 | const rfr = require('rfr') 2 | const chai = require('chai') 3 | const expect = chai.expect 4 | 5 | const request = rfr('./tests/helpers/request') 6 | const validators = rfr('./modules/v1/user/validators') 7 | 8 | describe('Module User: Validators', () => { 9 | let req 10 | let checkBody 11 | let notEmpty 12 | let len 13 | let isEmail 14 | let getValidationResult 15 | 16 | beforeEach(() => { 17 | req = request.stubReq() 18 | checkBody = request.stubCheckBody(req) 19 | notEmpty = request.stubNotEmpty(req) 20 | len = request.stubLen(req) 21 | isEmail = request.stubIsEmail(req) 22 | getValidationResult = request.stubGetValidationResult(req) 23 | }) 24 | 25 | afterEach(() => { 26 | checkBody.restore() 27 | notEmpty.restore() 28 | len.restore() 29 | isEmail.restore() 30 | getValidationResult.restore() 31 | }) 32 | 33 | it('should have all the required methods registred', () => { 34 | expect(validators).to.contain.all.keys(['create', 'replace', 'update', 'email', 'uniqueEmailValidator']) 35 | }) 36 | 37 | describe('Method Create', () => { 38 | beforeEach(() => { 39 | validators.create(req, request.res, () => {}) 40 | }) 41 | it('should be a function', () => { 42 | expect(validators.create).to.be.a('function') 43 | }) 44 | it('should call checkBody 10 times', () => { 45 | expect(checkBody.called).to.equal(true) 46 | expect(checkBody.callCount).to.equal(10) 47 | }) 48 | it('should call len 3 times', () => { 49 | expect(len.called).to.equal(true) 50 | expect(len.callCount).to.equal(3) 51 | }) 52 | it('should call notEmpty 5 times', () => { 53 | expect(notEmpty.called).to.equal(true) 54 | expect(notEmpty.callCount).to.equal(5) 55 | }) 56 | it('should call isEmail once', () => { 57 | expect(isEmail.called).to.equal(true) 58 | expect(isEmail.callCount).to.equal(1) 59 | }) 60 | it('should verify name required', () => { 61 | expect(checkBody.calledWith('name', {error: 'required'})).to.equal(true) 62 | }) 63 | it('should verify name length', () => { 64 | expect(checkBody.calledWith('name', {error: 'length', min: 4, max: 20})).to.equal(true) 65 | }) 66 | it('should verify email required', () => { 67 | expect(checkBody.calledWith('email', {error: 'required'})).to.equal(true) 68 | }) 69 | it('should verify email valid', () => { 70 | expect(checkBody.calledWith('email', {error: 'invalid'})).to.equal(true) 71 | }) 72 | it('should verify password required', () => { 73 | expect(checkBody.calledWith('password', {error: 'required'})).to.equal(true) 74 | }) 75 | it('should verify password length', () => { 76 | expect(checkBody.calledWith('password', {error: 'length', min: 6, max: 20})).to.equal(true) 77 | }) 78 | }) 79 | 80 | describe('Method Replace', () => { 81 | beforeEach(() => { 82 | validators.replace(req, request.res, () => {}) 83 | }) 84 | it('should be a function', () => { 85 | expect(validators.create).to.be.a('function') 86 | }) 87 | it('should call checkBody 10 times', () => { 88 | expect(checkBody.called).to.equal(true) 89 | expect(checkBody.callCount).to.equal(10) 90 | }) 91 | it('should call len 3 times', () => { 92 | expect(len.called).to.equal(true) 93 | expect(len.callCount).to.equal(3) 94 | }) 95 | it('should call notEmpty 5 times', () => { 96 | expect(notEmpty.called).to.equal(true) 97 | expect(notEmpty.callCount).to.equal(5) 98 | }) 99 | it('should call isEmail once', () => { 100 | expect(isEmail.called).to.equal(true) 101 | expect(isEmail.callCount).to.equal(1) 102 | }) 103 | it('should verify name required', () => { 104 | expect(checkBody.calledWith('name', {error: 'required'})).to.equal(true) 105 | }) 106 | it('should verify name length', () => { 107 | expect(checkBody.calledWith('name', {error: 'length', min: 4, max: 20})).to.equal(true) 108 | }) 109 | it('should verify email required', () => { 110 | expect(checkBody.calledWith('email', {error: 'required'})).to.equal(true) 111 | }) 112 | it('should verify email valid', () => { 113 | expect(checkBody.calledWith('email', {error: 'invalid'})).to.equal(true) 114 | }) 115 | it('should verify password required', () => { 116 | expect(checkBody.calledWith('password', {error: 'required'})).to.equal(true) 117 | }) 118 | it('should verify password length', () => { 119 | expect(checkBody.calledWith('password', {error: 'length', min: 6, max: 20})).to.equal(true) 120 | }) 121 | }) 122 | 123 | describe('Method Update', () => { 124 | beforeEach(() => { 125 | req['body'] = {email: '2--01', password: 'abcde', username: 'a', active: 'NOT A BOOLEAN', name: 'asd'} 126 | validators.update(req, request.res, () => {}) 127 | }) 128 | it('should be a function', () => { 129 | expect(validators.update).to.be.a('function') 130 | }) 131 | it('should call checkBody 5 times', () => { 132 | expect(checkBody.called).to.equal(true) 133 | expect(checkBody.callCount).to.equal(5) 134 | }) 135 | it('should call len 3 times', () => { 136 | expect(len.called).to.equal(true) 137 | expect(len.callCount).to.equal(3) 138 | }) 139 | it('should call isEmail once', () => { 140 | expect(isEmail.called).to.equal(true) 141 | expect(isEmail.callCount).to.equal(1) 142 | }) 143 | it('should verify username length', () => { 144 | expect(checkBody.calledWith('username', {error: 'length', min: 4, max: 20})).to.equal(true) 145 | }) 146 | it('should verify name length', () => { 147 | expect(checkBody.calledWith('name', {error: 'length', min: 4, max: 20})).to.equal(true) 148 | }) 149 | it('should verify password length', () => { 150 | expect(checkBody.calledWith('password', {error: 'length', min: 6, max: 20})).to.equal(true) 151 | }) 152 | it('should verify email valid', () => { 153 | expect(checkBody.calledWith('email', {error: 'invalid'})).to.equal(true) 154 | }) 155 | }) 156 | 157 | describe('Method Email', () => { 158 | beforeEach(() => { 159 | validators.email(req, request.res, () => {}) 160 | }) 161 | it('should be a function', () => { 162 | expect(validators.create).to.be.a('function') 163 | }) 164 | it('should call checkBody twice', () => { 165 | expect(checkBody.called).to.equal(true) 166 | expect(checkBody.callCount).to.equal(2) 167 | }) 168 | it('should call isEmail once', () => { 169 | expect(isEmail.called).to.equal(true) 170 | expect(isEmail.callCount).to.equal(1) 171 | }) 172 | it('should verify email required', () => { 173 | expect(checkBody.calledWith('email', {error: 'required'})).to.equal(true) 174 | }) 175 | it('should verify email valid', () => { 176 | expect(checkBody.calledWith('email', {error: 'invalid'})).to.equal(true) 177 | }) 178 | }) 179 | }) 180 | -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gfviegas/express-rest-api/186048efc5baa70c252deb695aa7be5e1c726ab3/tmp/.gitkeep --------------------------------------------------------------------------------