├── .eslintignore ├── .prettierignore ├── .dockerignore ├── src ├── env.js ├── server.js ├── models │ └── movie.js ├── routes │ ├── index.js │ └── movie.js ├── database.js ├── app.js ├── controllers │ └── movie.js └── middlewares │ └── logger │ └── index.js ├── .mocharc.yml ├── prettier.config.js ├── sample.env ├── script └── init-mongo.js ├── .babelrc ├── README.md ├── Dockerfile ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── docker-compose.yml ├── LICENSE ├── package.json └── test ├── integration └── routes │ └── movie.spec.js └── unit └── controllers └── movie.spec.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /script/*.js 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /script/*.js 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | mongo-volume/ 4 | .config/ 5 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | dotenv.config({ silent: true }); 4 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | reporter: spec 2 | require: '@babel/register' 3 | slow: 5000 4 | exit: true 5 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | }; 5 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | 3 | DATABASE_HOSTNAME=localhost:27017 4 | DATABASE_USERNAME=user 5 | DATABASE_PASSWORD=pswd 6 | DATABASE_DATABASE=database 7 | -------------------------------------------------------------------------------- /script/init-mongo.js: -------------------------------------------------------------------------------- 1 | db.createUser({ 2 | user: 'user', 3 | pwd: 'pswd', 4 | roles: [ 5 | { 6 | role: 'readWrite', 7 | db: 'database', 8 | }, 9 | ], 10 | }) 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": true 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lean API RESTFul - NodeJS 2 | 3 | The guide for develop a light WEB API RESTFul with NodeJS. 4 | 5 | ## Get Started 6 | 7 | ### Requirements 8 | 9 | ### Installation 10 | 11 | ### Tests 12 | 13 | 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.1 2 | 3 | ENV HOME=/home/app 4 | 5 | RUN apt-get update 6 | 7 | COPY package.json package-lock.json $HOME/ 8 | 9 | WORKDIR $HOME 10 | 11 | RUN npm install --silent 12 | 13 | COPY . $HOME/ 14 | 15 | CMD ["npm", "start"] 16 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | 3 | const port = process.env.PORT || 3000; 4 | 5 | try { 6 | app.listen(port, () => console.info(`Listening on port ${port}`)); 7 | } catch (error) { 8 | console.error(error); 9 | process.exit(1); 10 | } 11 | -------------------------------------------------------------------------------- /src/models/movie.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const schema = new mongoose.Schema({ 4 | title: String, 5 | description: String, 6 | director: String, 7 | year: String, 8 | }); 9 | 10 | const Movie = mongoose.model('Movie', schema); 11 | 12 | export default Movie; 13 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import movieRouter from './movie'; 3 | 4 | const router = new Router(); 5 | 6 | router.get('/healths', (req, res) => res.status(200).json({ status: 'UP' })); 7 | router.use('/movies', movieRouter); 8 | 9 | export default router; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /src/routes/movie.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import movieController from '../controllers/movie'; 3 | 4 | const router = new Router(); 5 | 6 | router.get('/', (req, res) => movieController.get(req, res)); 7 | router.get('/:id', (req, res) => movieController.getById(req, res)); 8 | router.post('/', (req, res) => movieController.create(req, res)); 9 | router.put('/:id', (req, res) => movieController.update(req, res)); 10 | router.delete('/:id', (req, res) => movieController.delete(req, res)); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": ["airbnb-base", "prettier"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["prettier"], 18 | "rules": { 19 | "prettier/prettier": "error", 20 | "no-console": "off", 21 | "class-methods-use-this": "off", 22 | "no-param-reassign": "off", 23 | "camelcase": "off", 24 | "no-unused-vars": ["error", { "argsIgnorePattern": "next" }] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Diagnostic reports (https://nodejs.org/api/report.html) 7 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | *.lcov 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Dependency directories 23 | node_modules/ 24 | jspm_packages/ 25 | 26 | # Optional npm cache directory 27 | .npm 28 | 29 | # Optional eslint cache 30 | .eslintcache 31 | 32 | # dotenv environment variables file 33 | .env 34 | .env.test 35 | 36 | # VS Code, Docs and Misc 37 | .vscode 38 | .ds_store 39 | docs/ 40 | dist/ 41 | mongo-volume/ 42 | .config/ 43 | -------------------------------------------------------------------------------- /src/database.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const uri = `mongodb://${process.env.DATABASE_USERNAME}1:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOSTNAME}/${process.env.DATABASE_DATABASE}`; 4 | 5 | class Database { 6 | constructor() { 7 | this.init(); 8 | } 9 | 10 | init() { 11 | mongoose 12 | .connect(uri, { 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true, 15 | }) 16 | .then(() => { 17 | console.info('Database connection successfully'); 18 | }) 19 | .catch((err) => { 20 | console.error('Database connection fail'); 21 | console.error(err); 22 | }); 23 | } 24 | } 25 | 26 | export default new Database(); 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | container_name: 'nodejs-api' 6 | environment: 7 | NODE_ENV: development 8 | volumes: 9 | - .:/home/app 10 | - /home/app/node_modules 11 | ports: 12 | - '8080:8080' 13 | depends_on: 14 | - database 15 | database: 16 | image: 'mongo:4.2' 17 | container_name: 'nodejs-api-db' 18 | environment: 19 | - MONGO_INITDB_DATABASE=database 20 | - MONGO_INITDB_ROOT_USERNAME=admin 21 | - MONGO_INITDB_ROOT_PASSWORD=pswrd 22 | volumes: 23 | - ./script/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro 24 | - ./mongo-volume:/data/db 25 | ports: 26 | - '27017-27019:27017-27019' 27 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import './env'; 2 | import './database'; 3 | import express from 'express'; 4 | import bodyParser from 'body-parser'; 5 | import helmet from 'helmet'; 6 | import morgan from 'morgan'; 7 | import routes from './routes'; 8 | import { loggerMiddleware } from './middlewares/logger'; 9 | 10 | class App { 11 | constructor() { 12 | this.server = express(); 13 | this.middlewares(); 14 | this.routes(); 15 | } 16 | 17 | middlewares() { 18 | this.server.use(bodyParser.json()); 19 | this.server.use(morgan('combined')); 20 | this.server.use(helmet()); 21 | this.server.use(loggerMiddleware); 22 | } 23 | 24 | routes() { 25 | this.server.use(routes); 26 | } 27 | } 28 | 29 | export default new App().server; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Nader Dabit 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /src/controllers/movie.js: -------------------------------------------------------------------------------- 1 | import Movie from '../models/movie'; 2 | 3 | class MovieController { 4 | async get(req, res) { 5 | try { 6 | const movies = await Movie.find({}); 7 | res.send(movies); 8 | } catch (err) { 9 | res.status(400).send(err.message); 10 | } 11 | } 12 | 13 | async getById(req, res) { 14 | try { 15 | const { 16 | params: { id }, 17 | } = req; 18 | 19 | const movie = await Movie.find({ _id: id }); 20 | res.send(movie); 21 | } catch (err) { 22 | res.status(400).send(err.message); 23 | } 24 | } 25 | 26 | async create(req, res) { 27 | const movie = new Movie(req.body); 28 | try { 29 | await movie.save(); 30 | res.status(201).send(movie); 31 | } catch (err) { 32 | res.status(422).send(err.message); 33 | } 34 | } 35 | 36 | async update(req, res) { 37 | try { 38 | await Movie.updateOne({ _id: req.params.id }, req.body); 39 | res.sendStatus(200); 40 | } catch (err) { 41 | res.status(422).send(err.message); 42 | } 43 | } 44 | 45 | async delete(req, res) { 46 | try { 47 | await Movie.deleteOne({ _id: req.params.id }); 48 | res.sendStatus(204); 49 | } catch (err) { 50 | res.status(400).send(err.message); 51 | } 52 | } 53 | } 54 | 55 | export default new MovieController(); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lean-nodejs-api-restful", 3 | "version": "1.0.0", 4 | "description": "api restful nodejs", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "start": "npm run build && node dist/server.js", 11 | "start:dev": "nodemon --exec babel-node src/server.js", 12 | "build": "babel src --out-dir dist", 13 | "test:unit": "NODE_ENV=test mocha test/unit/**/*.spec.js", 14 | "test:integration": "NODE_ENV=test mocha test/integration/**/*.spec.js", 15 | "lint": "eslint src --ext .js", 16 | "lint:fix": "eslint src --fix --ext .js", 17 | "prettier": "prettier --check 'src/**/*.js'", 18 | "prettier:fix": "prettier --write 'src/**/*.js'", 19 | "prepare": "npm run lint:fix && npm run prettier:fix" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/danielcsrs/lean-nodejs-api-restful.git" 24 | }, 25 | "keywords": [ 26 | "nodejs", 27 | "express", 28 | "mongo" 29 | ], 30 | "author": "@danielcsrs", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/danielcsrs/lean-nodejs-api-restful/issues" 34 | }, 35 | "homepage": "https://github.com/danielcsrs/lean-nodejs-api-restful#readme", 36 | "dependencies": { 37 | "body-parser": "^1.19.0", 38 | "dotenv": "^8.2.0", 39 | "express": "^4.17.1", 40 | "helmet": "^3.22.0", 41 | "moment": "^2.24.0", 42 | "mongoose": "^5.9.7", 43 | "morgan": "^1.10.0", 44 | "winston": "^3.2.1" 45 | }, 46 | "devDependencies": { 47 | "@babel/cli": "^7.8.4", 48 | "@babel/core": "^7.9.0", 49 | "@babel/node": "^7.8.7", 50 | "@babel/preset-env": "^7.9.0", 51 | "chai": "^4.2.0", 52 | "eslint": "^6.8.0", 53 | "eslint-config-airbnb-base": "^14.1.0", 54 | "eslint-config-prettier": "^6.10.1", 55 | "eslint-plugin-import": "^2.20.2", 56 | "eslint-plugin-prettier": "^3.1.2", 57 | "mocha": "^7.1.1", 58 | "nodemon": "^2.0.2", 59 | "prettier": "^2.0.4", 60 | "sinon": "^9.0.2", 61 | "supertest": "^4.0.2" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/integration/routes/movie.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-return-await */ 2 | /* eslint-disable no-undef */ 3 | /* eslint-disable no-underscore-dangle */ 4 | import supertest from 'supertest'; 5 | import chai from 'chai'; 6 | import setupApp from '../../../src/app'; 7 | import Movie from '../../../src/models/movie'; 8 | 9 | describe('Routes: Movies', () => { 10 | before(async () => { 11 | const app = await setupApp(); 12 | global.app = app; 13 | global.request = supertest(app); 14 | global.expect = chai.expect; 15 | }); 16 | 17 | after(async () => await app.database.connection.close()); 18 | 19 | const defaultId = '5e90f0600bf2272ecf8e82d2'; 20 | const defaultMovie = { 21 | title: 'Star Wars A New Hope', 22 | description: 'Loren ipsun dolor', 23 | year: '1977', 24 | director: 'George Lucas', 25 | }; 26 | const expectedMovie = { 27 | __v: 0, 28 | _id: '5e90f0600bf2272ecf8e82d2', 29 | title: 'Star Wars A New Hope', 30 | description: 'Loren ipsun dolor', 31 | year: '1977', 32 | director: 'George Lucas', 33 | }; 34 | 35 | beforeEach(async () => { 36 | await Movie.deleteMany(); 37 | 38 | const movie = new Movie(defaultMovie); 39 | movie._id = defaultId; 40 | return movie.save(); 41 | }); 42 | 43 | describe('GET /', () => { 44 | it('should return a list of movies', (done) => { 45 | request.get('/movies').end((err, res) => { 46 | expect(res.statusCode).to.eql(200); 47 | expect(res.body).to.eql([expectedMovie]); 48 | done(err); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('GET /', () => { 54 | it('should return 200 with one movie', (done) => { 55 | request.get(`/movies/${defaultId}`).end((err, res) => { 56 | expect(res.statusCode).to.eql(200); 57 | expect(res.body).to.eql([expectedMovie]); 58 | done(err); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('GET /', () => { 64 | it('should return a hello world', (done) => { 65 | request.get('/').end((err, res) => { 66 | expect(res.statusCode).to.eql(200); 67 | expect(res.body).to.eql({}); 68 | expect(res.text).to.eql('Hello World!'); 69 | done(err); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/middlewares/logger/index.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import moment from 'moment'; 3 | 4 | const { createLogger, format, transports } = winston; 5 | 6 | const { colorize, combine, label, printf, timestamp } = format; 7 | 8 | const isLabel = (info) => (info.label ? `[_${info.label}_]` : ''); 9 | 10 | const defaultFormat = printf( 11 | (info) => 12 | `${moment(info.timestamp).format('MMM D, HH:mm:ss:SSS')} - ${isLabel( 13 | info 14 | )} [_${info.level}_] : ${info.message}` 15 | ); 16 | 17 | const myCustomLevels = { 18 | levels: { 19 | fatal: 0, 20 | error: 1, 21 | warn: 2, 22 | success: 3, 23 | info: 4, 24 | }, 25 | colors: { 26 | fatal: 'magenta', 27 | error: 'red', 28 | warn: 'yellow', 29 | success: 'green', 30 | info: 'blue', 31 | }, 32 | }; 33 | 34 | winston.addColors(myCustomLevels.colors); 35 | 36 | const combineProd = (tag) => 37 | combine(timestamp(), label({ label: tag }), defaultFormat); 38 | 39 | const combineDev = (tag) => 40 | combine(timestamp(), colorize(), label({ label: tag }), defaultFormat); 41 | 42 | const getCombine = (tag) => 43 | process.env.NODE_ENV === 'Production' ? combineProd(tag) : combineDev(tag); 44 | 45 | const getLogger = (tag, level) => 46 | createLogger({ 47 | format: getCombine(tag), 48 | transports: [new transports.Console({ level })], 49 | levels: myCustomLevels.levels, 50 | silent: process.env.NODE_ENV === 'Test', 51 | }); 52 | 53 | const makeLogger = (tag, level) => { 54 | const logger = getLogger(tag, level); 55 | 56 | function makeLogFunction(name) { 57 | return (data) => logger[name](JSON.stringify(data)); 58 | } 59 | 60 | return { 61 | fatal: makeLogFunction('fatal'), 62 | error: makeLogFunction('error'), 63 | warn: makeLogFunction('warn'), 64 | success: makeLogFunction('success'), 65 | info: makeLogFunction('info'), 66 | }; 67 | }; 68 | 69 | const loggerMiddleware = (req, res, next) => { 70 | const requestId = 71 | req.get('request_id') || 72 | req.body.requestId || 73 | Math.round(Math.random() * (100000 - 1000)); 74 | 75 | const logger = makeLogger(String(requestId).trim(), process.env.LOG_LEVEL); 76 | 77 | req.logger = logger; 78 | 79 | return next(); 80 | }; 81 | 82 | const logger = getLogger(); 83 | 84 | export { loggerMiddleware, logger }; 85 | -------------------------------------------------------------------------------- /test/unit/controllers/movie.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | /* eslint-disable class-methods-use-this */ 3 | import sinon from 'sinon'; 4 | import MovieController from '../../../src/controllers/movie'; 5 | import Movie from '../../../src/models/movie'; 6 | 7 | describe('Controller: Movie', () => { 8 | const defaultMovie = { 9 | __v: 0, 10 | _id: '5e90f0600bf2272ecf8e82d2', 11 | name: 'Star Wars A New Hope', 12 | description: 'Loren ipsun dolor', 13 | year: 1977, 14 | }; 15 | 16 | const defaultRequest = { 17 | params: {}, 18 | }; 19 | 20 | describe('get()', () => { 21 | it('should return a list of movies', async () => { 22 | const response = { 23 | send: sinon.spy(), 24 | }; 25 | 26 | Movie.find = sinon.stub(); 27 | Movie.find.withArgs({}).resolves(defaultMovie); 28 | 29 | const movieController = new MovieController(Movie); 30 | 31 | await movieController.get(defaultRequest, response); 32 | 33 | sinon.assert.calledWith(response.send, defaultMovie); 34 | }); 35 | 36 | it('should return 400 when an error occurs', async () => { 37 | const request = {}; 38 | const response = { 39 | send: sinon.spy(), 40 | status: sinon.stub(), 41 | }; 42 | 43 | response.status.withArgs(400).returns(response); 44 | 45 | Movie.find = sinon.stub(); 46 | Movie.find.withArgs({}).rejects({ message: 'Error' }); 47 | 48 | const movieController = new MovieController(Movie); 49 | 50 | await movieController.get(request, response); 51 | 52 | sinon.assert.calledWith(response.send, 'Error'); 53 | }); 54 | }); 55 | 56 | describe('getById()', () => { 57 | it('should return a one movie', async () => { 58 | const fakeId = 'fake-id'; 59 | const request = { 60 | params: { 61 | id: fakeId, 62 | }, 63 | }; 64 | const response = { 65 | send: sinon.spy(), 66 | }; 67 | 68 | Movie.find = sinon.stub(); 69 | Movie.find.withArgs({ _id: fakeId }).resolves(defaultMovie); 70 | 71 | const movieController = new MovieController(Movie); 72 | await movieController.getById(request, response); 73 | 74 | sinon.assert.calledWith(response.send, defaultMovie); 75 | }); 76 | 77 | it('should return 400 when an error occurs', async () => { 78 | const fakeId = 'fake-id'; 79 | const request = { 80 | params: { 81 | id: fakeId, 82 | }, 83 | }; 84 | const response = { 85 | send: sinon.spy(), 86 | status: sinon.stub(), 87 | }; 88 | 89 | response.status.withArgs(400).returns(response); 90 | 91 | Movie.find = sinon.stub(); 92 | Movie.find.withArgs({ _id: fakeId }).rejects({ message: 'Error' }); 93 | 94 | const movieController = new MovieController(Movie); 95 | 96 | await movieController.getById(request, response); 97 | 98 | sinon.assert.calledWith(response.send, 'Error'); 99 | }); 100 | }); 101 | 102 | describe('create()', () => { 103 | it('should save a new movie successfully', async () => { 104 | const request = { 105 | body: defaultMovie[0], 106 | ...defaultMovie, 107 | }; 108 | const response = { 109 | send: sinon.spy(), 110 | status: sinon.stub(), 111 | }; 112 | class fakeMovie { 113 | save() {} 114 | } 115 | response.status.withArgs(201).returns(response); 116 | sinon.stub(fakeMovie.prototype, 'save').withArgs().resolves(); 117 | 118 | const movieController = new MovieController(fakeMovie); 119 | 120 | await movieController.create(request, response); 121 | 122 | sinon.assert.calledWith(response.send); 123 | }); 124 | 125 | it('should return a 422 when an error occurs', async () => { 126 | const response = { 127 | send: sinon.spy(), 128 | status: sinon.stub(), 129 | }; 130 | 131 | class fakeMovie { 132 | save() {} 133 | } 134 | 135 | response.status.withArgs(422).returns(response); 136 | 137 | sinon 138 | .stub(fakeMovie.prototype, 'save') 139 | .withArgs() 140 | .rejects({ message: 'Error' }); 141 | 142 | const movieController = new MovieController(fakeMovie); 143 | 144 | await movieController.create(defaultMovie, response); 145 | sinon.assert.calledWith(response.status, 422); 146 | }); 147 | }); 148 | 149 | describe('update()', () => { 150 | const fakeId = 'fake-id'; 151 | const updatedMovie = { 152 | _id: fakeId, 153 | name: 'Updated movie', 154 | description: 'Updated description', 155 | year: 2020, 156 | }; 157 | 158 | it('should respond with 200 when the movie has been updated', async () => { 159 | const request = { 160 | params: { 161 | id: fakeId, 162 | }, 163 | body: updatedMovie, 164 | }; 165 | const response = { 166 | sendStatus: sinon.spy(), 167 | }; 168 | 169 | class fakeMovie { 170 | static updateOne() {} 171 | } 172 | 173 | const updateOneStub = sinon.stub(fakeMovie, 'updateOne'); 174 | 175 | updateOneStub 176 | .withArgs({ _id: fakeId }, updatedMovie) 177 | .resolves(updatedMovie); 178 | 179 | const movieController = new MovieController(fakeMovie); 180 | 181 | await movieController.update(request, response); 182 | sinon.assert.calledWith(response.sendStatus, 200); 183 | }); 184 | 185 | it('should return a 422 when an error occurs', async () => { 186 | const request = { 187 | params: { 188 | id: fakeId, 189 | }, 190 | body: updatedMovie, 191 | }; 192 | const response = { 193 | send: sinon.spy(), 194 | status: sinon.stub(), 195 | }; 196 | 197 | class fakeMovie { 198 | static updateOne() {} 199 | } 200 | 201 | const updateOneStub = sinon.stub(fakeMovie, 'updateOne'); 202 | 203 | updateOneStub 204 | .withArgs({ _id: fakeId }, updatedMovie) 205 | .rejects({ message: 'Error' }); 206 | 207 | response.status.withArgs(422).returns(response); 208 | 209 | const movieController = new MovieController(fakeMovie); 210 | 211 | await movieController.update(request, response); 212 | sinon.assert.calledWith(response.send, 'Error'); 213 | }); 214 | }); 215 | 216 | describe('delete()', () => { 217 | it('should respond with 204 when the movie has been deleted', async () => { 218 | const fakeId = 'fake-id'; 219 | const request = { 220 | params: { 221 | id: fakeId, 222 | }, 223 | }; 224 | const response = { 225 | sendStatus: sinon.spy(), 226 | }; 227 | 228 | class fakeMovie { 229 | static deleteOne() {} 230 | } 231 | 232 | const deleteOneStub = sinon.stub(fakeMovie, 'deleteOne'); 233 | 234 | deleteOneStub.withArgs({ _id: fakeId }).resolves(); 235 | 236 | const movieController = new MovieController(fakeMovie); 237 | 238 | await movieController.delete(request, response); 239 | 240 | sinon.assert.calledWith(response.sendStatus, 204); 241 | }); 242 | 243 | it('should return a 400 when an error occurs', async () => { 244 | const fakeId = 'fake-id'; 245 | const request = { 246 | params: { 247 | id: fakeId, 248 | }, 249 | }; 250 | 251 | const response = { 252 | send: sinon.spy(), 253 | status: sinon.stub(), 254 | }; 255 | 256 | class fakeMovie { 257 | static deleteOne() {} 258 | } 259 | 260 | const deleteOneStub = sinon.stub(fakeMovie, 'deleteOne'); 261 | 262 | deleteOneStub.withArgs({ _id: fakeId }).rejects({ message: 'Error' }); 263 | response.status.withArgs(400).returns(response); 264 | 265 | const movieController = new MovieController(fakeMovie); 266 | 267 | await movieController.delete(request, response); 268 | sinon.assert.calledWith(response.send, 'Error'); 269 | }); 270 | }); 271 | }); 272 | --------------------------------------------------------------------------------