├── .gitignore ├── Procfile ├── README.md ├── controllers ├── login.js ├── notes.js └── users.js ├── images └── logo.png ├── index.js ├── middleware ├── handleErrors.js ├── notFound.js └── userExtractor.js ├── models ├── Note.js └── User.js ├── mongo.js ├── package.json ├── requests ├── create_user.rest ├── delete_note.rest ├── get_all_notes.rest ├── get_all_users.rest ├── login_user.rest ├── post_note.rest └── put_note.rest ├── tests ├── average.test.js ├── helpers.js ├── notes.test.js ├── palindrome.test.js ├── suma_test.js └── user.test.js └── utils └── for_testing.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backend de FullStack Bootcamp 2 | 3 | **Curso FullStack Bootcamp JavaScript gratuito:** https://www.youtube.com/watch?v=wTpuKOhGfJE&list=PLV8x_i1fqBw0Kn_fBIZTa3wS_VZAqddX7 4 | 5 | **Código en master: Código de la clase de MongoDB** 6 | 7 | Otras ramas con código: 8 | - [Código de la clase de Node y Express](https://github.com/midudev/notes-api/tree/clase-node-express) 9 | - [Código de la clase de MongoDB](https://github.com/midudev/notes-api/tree/clase-mongodb) -------------------------------------------------------------------------------- /controllers/login.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const bcrypt = require('bcrypt') 3 | const loginRouter = require('express').Router() 4 | const User = require('../models/User') 5 | 6 | loginRouter.post('/', async (request, response) => { 7 | const { body } = request 8 | const { username, password } = body 9 | 10 | const user = await User.findOne({ username }) 11 | 12 | const passwordCorrect = user === null 13 | ? false 14 | : await bcrypt.compare(password, user.passwordHash) 15 | 16 | if (!(user && passwordCorrect)) { 17 | response.status(401).json({ 18 | error: 'invalid user or password' 19 | }) 20 | } 21 | 22 | const userForToken = { 23 | id: user._id, 24 | username: user.username 25 | } 26 | 27 | const token = jwt.sign( 28 | userForToken, 29 | process.env.SECRET, 30 | { 31 | expiresIn: 60 * 60 * 24 * 7 32 | } 33 | ) 34 | 35 | response.send({ 36 | name: user.name, 37 | username: user.username, 38 | token 39 | }) 40 | }) 41 | 42 | module.exports = loginRouter 43 | -------------------------------------------------------------------------------- /controllers/notes.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/notes-api/5bf27516c8dca829bfb30389f55adb2d931421d4/controllers/notes.js -------------------------------------------------------------------------------- /controllers/users.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const usersRouter = require('express').Router() 3 | const User = require('../models/User') 4 | 5 | usersRouter.get('/', async (request, response) => { 6 | const users = await User.find({}).populate('notes', { 7 | content: 1, 8 | date: 1 9 | }) 10 | response.json(users) 11 | }) 12 | 13 | usersRouter.post('/', async (request, response) => { 14 | const { body } = request 15 | const { username, name, password } = body 16 | 17 | const saltRounds = 10 18 | const passwordHash = await bcrypt.hash(password, saltRounds) 19 | 20 | const user = new User({ 21 | username, 22 | name, 23 | passwordHash 24 | }) 25 | 26 | const savedUser = await user.save() 27 | 28 | response.status(201).json(savedUser) 29 | }) 30 | 31 | module.exports = usersRouter 32 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/notes-api/5bf27516c8dca829bfb30389f55adb2d931421d4/images/logo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | require('./mongo') 3 | 4 | const Sentry = require('@sentry/node') 5 | const Tracing = require('@sentry/tracing') 6 | const express = require('express') 7 | const app = express() 8 | const cors = require('cors') 9 | const User = require('./models/User') 10 | const Note = require('./models/Note') 11 | 12 | const notFound = require('./middleware/notFound.js') 13 | const handleErrors = require('./middleware/handleErrors.js') 14 | const userExtractor = require('./middleware/userExtractor') 15 | 16 | const usersRouter = require('./controllers/users') 17 | const loginRouter = require('./controllers/login') 18 | 19 | app.use(cors()) 20 | app.use(express.json()) 21 | app.use('/images', express.static('images')) 22 | 23 | Sentry.init({ 24 | dsn: 'https://ac034ebd99274911a8234148642e044c@o537348.ingest.sentry.io/5655435', 25 | integrations: [ 26 | // enable HTTP calls tracing 27 | new Sentry.Integrations.Http({ tracing: true }), 28 | // enable Express.js middleware tracing 29 | new Tracing.Integrations.Express({ app }) 30 | ], 31 | 32 | // We recommend adjusting this value in production, or using tracesSampler 33 | // for finer control 34 | tracesSampleRate: 1.0 35 | }) 36 | 37 | // RequestHandler creates a separate execution context using domains, so that every 38 | // transaction/span/breadcrumb is attached to its own Hub instance 39 | app.use(Sentry.Handlers.requestHandler()) 40 | // TracingHandler creates a trace for every incoming request 41 | app.use(Sentry.Handlers.tracingHandler()) 42 | 43 | app.get('/', (request, response) => { 44 | console.log(request.ip) 45 | console.log(request.ips) 46 | console.log(request.originalUrl) 47 | response.send('

Hello World!

') 48 | }) 49 | 50 | app.get('/api/notes', async (request, response) => { 51 | const notes = await Note.find({}).populate('user', { 52 | username: 1, 53 | name: 1 54 | }) 55 | response.json(notes) 56 | }) 57 | 58 | app.get('/api/notes/:id', (request, response, next) => { 59 | const { id } = request.params 60 | 61 | Note.findById(id) 62 | .then(note => { 63 | if (note) return response.json(note) 64 | response.status(404).end() 65 | }) 66 | .catch(err => next(err)) 67 | }) 68 | 69 | app.put('/api/notes/:id', userExtractor, (request, response, next) => { 70 | const { id } = request.params 71 | const note = request.body 72 | 73 | const newNoteInfo = { 74 | content: note.content, 75 | important: note.important 76 | } 77 | 78 | Note.findByIdAndUpdate(id, newNoteInfo, { new: true }) 79 | .then(result => { 80 | response.json(result) 81 | }) 82 | .catch(next) 83 | }) 84 | 85 | app.delete('/api/notes/:id', userExtractor, async (request, response, next) => { 86 | const { id } = request.params 87 | // const note = await Note.findById(id) 88 | // if (!note) return response.sendStatus(404) 89 | 90 | const res = await Note.findByIdAndDelete(id) 91 | if (res === null) return response.sendStatus(404) 92 | 93 | response.status(204).end() 94 | }) 95 | 96 | app.post('/api/notes', userExtractor, async (request, response, next) => { 97 | const { 98 | content, 99 | important = false 100 | } = request.body 101 | 102 | // sacar userId de request 103 | const { userId } = request 104 | 105 | const user = await User.findById(userId) 106 | 107 | if (!content) { 108 | return response.status(400).json({ 109 | error: 'required "content" field is missing' 110 | }) 111 | } 112 | 113 | const newNote = new Note({ 114 | content, 115 | date: new Date(), 116 | important, 117 | user: user._id 118 | }) 119 | 120 | // newNote.save().then(savedNote => { 121 | // response.json(savedNote) 122 | // }).catch(err => next(err)) 123 | 124 | try { 125 | const savedNote = await newNote.save() 126 | 127 | user.notes = user.notes.concat(savedNote._id) 128 | await user.save() 129 | 130 | response.json(savedNote) 131 | } catch (error) { 132 | next(error) 133 | } 134 | }) 135 | 136 | app.use('/api/users', usersRouter) 137 | app.use('/api/login', loginRouter) 138 | 139 | app.use(notFound) 140 | 141 | app.use(Sentry.Handlers.errorHandler()) 142 | app.use(handleErrors) 143 | 144 | const PORT = process.env.PORT || 3001 145 | const server = app.listen(PORT, () => { 146 | console.log(`Server running on port ${PORT}`) 147 | }) 148 | 149 | module.exports = { app, server } 150 | -------------------------------------------------------------------------------- /middleware/handleErrors.js: -------------------------------------------------------------------------------- 1 | const ERROR_HANDLERS = { 2 | CastError: res => 3 | res.status(400).send({ error: 'id used is malformed' }), 4 | 5 | ValidationError: (res, { message }) => 6 | res.status(409).send({ error: message }), 7 | 8 | JsonWebTokenError: (res) => 9 | res.status(401).json({ error: 'token missing or invalid' }), 10 | 11 | TokenExpirerError: res => 12 | res.status(401).json({ error: 'token expired' }), 13 | 14 | defaultError: (res, error) => { 15 | console.error(error.name) 16 | res.status(500).end() 17 | } 18 | } 19 | 20 | module.exports = (error, request, response, next) => { 21 | const handler = 22 | ERROR_HANDLERS[error.name] || ERROR_HANDLERS.defaultError 23 | 24 | handler(response, error) 25 | } 26 | -------------------------------------------------------------------------------- /middleware/notFound.js: -------------------------------------------------------------------------------- 1 | module.exports = (request, response, next) => { 2 | response.status(404).end() 3 | } 4 | -------------------------------------------------------------------------------- /middleware/userExtractor.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | 3 | module.exports = (request, response, next) => { 4 | const authorization = request.get('authorization') 5 | let token = '' 6 | 7 | if (authorization && authorization.toLowerCase().startsWith('bearer')) { 8 | token = authorization.substring(7) 9 | } 10 | 11 | const decodedToken = jwt.verify(token, process.env.SECRET) 12 | 13 | if (!token || !decodedToken.id) { 14 | return response.status(401).json({ error: 'token missing or invalid' }) 15 | } 16 | 17 | const { id: userId } = decodedToken 18 | 19 | request.userId = userId 20 | 21 | next() 22 | } 23 | -------------------------------------------------------------------------------- /models/Note.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose') 2 | 3 | const noteSchema = new Schema({ 4 | content: String, 5 | date: Date, 6 | important: Boolean, 7 | user: { 8 | type: Schema.Types.ObjectId, 9 | ref: 'User' 10 | } 11 | }) 12 | 13 | noteSchema.set('toJSON', { 14 | transform: (document, returnedObject) => { 15 | returnedObject.id = returnedObject._id 16 | delete returnedObject._id 17 | delete returnedObject.__v 18 | } 19 | }) 20 | 21 | const Note = model('Note', noteSchema) 22 | 23 | module.exports = Note 24 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const { Schema, model } = require('mongoose') 2 | const uniqueValidator = require('mongoose-unique-validator') 3 | 4 | const userSchema = new Schema({ 5 | username: { 6 | type: String, 7 | unique: true 8 | }, 9 | name: String, 10 | passwordHash: String, 11 | notes: [{ 12 | type: Schema.Types.ObjectId, 13 | ref: 'Note' 14 | }] 15 | }) 16 | 17 | userSchema.set('toJSON', { 18 | transform: (document, returnedObject) => { 19 | returnedObject.id = returnedObject._id 20 | delete returnedObject._id 21 | delete returnedObject.__v 22 | 23 | delete returnedObject.passwordHash 24 | } 25 | }) 26 | 27 | userSchema.plugin(uniqueValidator) 28 | 29 | const User = model('User', userSchema) 30 | 31 | module.exports = User 32 | -------------------------------------------------------------------------------- /mongo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const { MONGO_DB_URI, MONGO_DB_URI_TEST, NODE_ENV } = process.env 4 | 5 | const connectionString = NODE_ENV === 'test' 6 | ? MONGO_DB_URI_TEST 7 | : MONGO_DB_URI 8 | 9 | // comment for students puposes 10 | if (!connectionString) { 11 | console.error('Recuerda que tienes que tener un archivo .env con las variables de entorno definidas y el MONGO_DB_URI que servirá de connection string. En las clases usamos MongoDB Atlas pero puedes usar cualquier base de datos de MongoDB (local incluso).') 12 | } 13 | 14 | // conexión a mongodb 15 | mongoose.connect(connectionString, { 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true, 18 | useFindAndModify: false, 19 | useCreateIndex: true 20 | }) 21 | .then(() => { 22 | console.log('Database connected') 23 | }).catch(err => { 24 | console.error(err) 25 | }) 26 | 27 | process.on('uncaughtException', error => { 28 | console.error(error) 29 | mongoose.disconnect() 30 | }) 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "part3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "NODE_ENV=development nodemon index.js", 8 | "lint": "npm run lint", 9 | "start": "NODE_ENV=production node index.js", 10 | "test": "NODE_ENV=test PORT=1234 jest --verbose tests/user.test.js", 11 | "test:watch": "npm run test -- --watch" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "@sentry/node": "6.2.0", 18 | "@sentry/tracing": "6.2.0", 19 | "bcrypt": "5.0.1", 20 | "cors": "2.8.5", 21 | "cross-env": "7.0.3", 22 | "dotenv": "8.2.0", 23 | "express": "4.17.1", 24 | "jsonwebtoken": "^8.5.1", 25 | "mongoose": "5.11.18", 26 | "mongoose-unique-validator": "2.0.3" 27 | }, 28 | "devDependencies": { 29 | "eslint": "7.20.0", 30 | "jest": "26.6.3", 31 | "nodemon": "2.0.7", 32 | "standard": "16.0.3", 33 | "supertest": "6.1.3" 34 | }, 35 | "eslintConfig": { 36 | "extends": "./node_modules/standard/eslintrc.json", 37 | "env": { 38 | "jest": true 39 | } 40 | }, 41 | "jest": { 42 | "testEnvironment": "node" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /requests/create_user.rest: -------------------------------------------------------------------------------- 1 | POST http://localhost:3001/api/users 2 | Content-Type: application/json 3 | 4 | { 5 | "username": "midudev", 6 | "name": "Miguel", 7 | "password": "lamidupassword" 8 | } -------------------------------------------------------------------------------- /requests/delete_note.rest: -------------------------------------------------------------------------------- 1 | DELETE http://localhost:3001/api/notes/603bf5fa1099e06f419b3984 -------------------------------------------------------------------------------- /requests/get_all_notes.rest: -------------------------------------------------------------------------------- 1 | GET http://localhost:3001/api/notes -------------------------------------------------------------------------------- /requests/get_all_users.rest: -------------------------------------------------------------------------------- 1 | GET http://localhost:3001/api/users -------------------------------------------------------------------------------- /requests/login_user.rest: -------------------------------------------------------------------------------- 1 | POST http://localhost:3001/api/login 2 | Content-Type: application/json 3 | 4 | { 5 | "username": "midudev", 6 | "password": "lamidupassword" 7 | } -------------------------------------------------------------------------------- /requests/post_note.rest: -------------------------------------------------------------------------------- 1 | POST http://localhost:3001/api/notes 2 | Content-Type: application/json 3 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwNTc5ZjY3MGUzMTBlODRkYWQxZjcyNiIsInVzZXJuYW1lIjoibWlkdWRldiIsImlhdCI6MTYxNjM1Nzc5MiwiZXhwIjoxNjE2OTYyNTkyfQ.k8Zor0K8hYzkDVfow66_MhRbmfA0ejbcW7AG3pqeQjs 4 | 5 | { 6 | "content": "Utilizando el middleware del user extractor", 7 | "important": true 8 | } -------------------------------------------------------------------------------- /requests/put_note.rest: -------------------------------------------------------------------------------- 1 | PUT http://localhost:3001/api/notes/603bfb496efbcb72db14c4da 2 | Content-Type: application/json 3 | 4 | { 5 | "content": "Suscríbete con PRIME que es gratishh", 6 | "important": true 7 | } -------------------------------------------------------------------------------- /tests/average.test.js: -------------------------------------------------------------------------------- 1 | const { average } = require('../utils/for_testing') 2 | 3 | describe.skip('average', () => { 4 | test('of one value is the value itself', () => { 5 | expect(average([1])).toBe(1) 6 | }) 7 | 8 | test('of many is calculated correctly', () => { 9 | expect(average([1, 2, 3, 4, 5, 6])).toBe(3.5) 10 | }) 11 | 12 | test('of empty array is zero', () => { 13 | expect(average([])).toBe(0) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | const { app } = require('../index') 2 | const supertest = require('supertest') 3 | const User = require('../models/User') 4 | 5 | const api = supertest(app) 6 | 7 | const initialNotes = [ 8 | { 9 | content: 'Aprendiendo FullStack JS con midudev', 10 | important: true, 11 | date: new Date() 12 | }, 13 | { 14 | content: 'Sígueme en https://midu.tube', 15 | important: true, 16 | date: new Date() 17 | }, 18 | { 19 | content: 'Gracias al chat por vuestra ayuda! :D', 20 | important: true, 21 | date: new Date() 22 | } 23 | ] 24 | 25 | const getAllContentFromNotes = async () => { 26 | const response = await api.get('/api/notes') 27 | return { 28 | contents: response.body.map(note => note.content), 29 | response 30 | } 31 | } 32 | 33 | const getUsers = async () => { 34 | const usersDB = await User.find({}) 35 | return usersDB.map(user => user.toJSON()) 36 | } 37 | 38 | module.exports = { 39 | api, 40 | initialNotes, 41 | getAllContentFromNotes, 42 | getUsers 43 | } 44 | -------------------------------------------------------------------------------- /tests/notes.test.js: -------------------------------------------------------------------------------- 1 | const moongose = require('mongoose') 2 | 3 | const { server } = require('../index') 4 | const Note = require('../models/Note') 5 | const { 6 | api, 7 | initialNotes, 8 | getAllContentFromNotes 9 | } = require('./helpers') 10 | 11 | beforeEach(async () => { 12 | await Note.deleteMany({}) 13 | 14 | // sequential 15 | for (const note of initialNotes) { 16 | const noteObject = new Note(note) 17 | await noteObject.save() 18 | } 19 | }) 20 | 21 | describe('GET all notes', () => { 22 | test('notes are returned as json', async () => { 23 | await api 24 | .get('/api/notes') 25 | .expect(200) 26 | .expect('Content-Type', /application\/json/) 27 | }) 28 | 29 | test('there are two notes', async () => { 30 | const response = await api.get('/api/notes') 31 | expect(response.body).toHaveLength(initialNotes.length) 32 | }) 33 | 34 | test('the first note is about midudev', async () => { 35 | const { 36 | contents 37 | } = await getAllContentFromNotes() 38 | 39 | expect(contents).toContain('Aprendiendo FullStack JS con midudev') 40 | }) 41 | }) 42 | 43 | describe('create a note', () => { 44 | test('is possible with a valid note', async () => { 45 | const newNote = { 46 | content: 'Proximamente async/await', 47 | important: true 48 | } 49 | 50 | await api 51 | .post('/api/notes') 52 | .send(newNote) 53 | .expect(200) 54 | .expect('Content-Type', /application\/json/) 55 | 56 | const { contents, response } = await getAllContentFromNotes() 57 | 58 | expect(response.body).toHaveLength(initialNotes.length + 1) 59 | expect(contents).toContain(newNote.content) 60 | }) 61 | 62 | test('is not possible with an invalid note', async () => { 63 | const newNote = { 64 | important: true 65 | } 66 | 67 | await api 68 | .post('/api/notes') 69 | .send(newNote) 70 | .expect(400) 71 | 72 | const response = await api.get('/api/notes') 73 | 74 | expect(response.body).toHaveLength(initialNotes.length) 75 | }) 76 | }) 77 | 78 | test('a note can be deleted', async () => { 79 | const { response: firstResponse } = await getAllContentFromNotes() 80 | const { body: notes } = firstResponse 81 | const noteToDelete = notes[0] 82 | 83 | await api 84 | .delete(`/api/notes/${noteToDelete.id}`) 85 | .expect(204) 86 | 87 | const { contents, response: secondResponse } = await getAllContentFromNotes() 88 | 89 | expect(secondResponse.body).toHaveLength(initialNotes.length - 1) 90 | 91 | expect(contents).not.toContain(noteToDelete.content) 92 | }) 93 | 94 | test('a note that has an invalid id can not be deleted', async () => { 95 | await api 96 | .delete('/api/notes/1234') 97 | .expect(400) 98 | 99 | const { response } = await getAllContentFromNotes() 100 | 101 | expect(response.body).toHaveLength(initialNotes.length) 102 | }) 103 | 104 | test('a note that has a valid id but do not exist can not be deleted', async () => { 105 | const validObjectIdThatDoNotExist = '60451827152dc22ad768f442' 106 | await api 107 | .delete(`/api/notes/${validObjectIdThatDoNotExist}`) 108 | .expect(404) 109 | 110 | const { response } = await getAllContentFromNotes() 111 | 112 | expect(response.body).toHaveLength(initialNotes.length) 113 | }) 114 | 115 | afterAll(() => { 116 | moongose.connection.close() 117 | server.close() 118 | }) 119 | -------------------------------------------------------------------------------- /tests/palindrome.test.js: -------------------------------------------------------------------------------- 1 | const { palindrome } = require('../utils/for_testing') 2 | 3 | test.skip('palindrome of midudev', () => { 4 | const result = palindrome('midudev') 5 | 6 | expect(result).toBe('vedudim') 7 | }) 8 | 9 | test.skip('palindrome of empty string', () => { 10 | const result = palindrome('') 11 | 12 | expect(result).toBe('') 13 | }) 14 | 15 | test.skip('palindrome of undefined', () => { 16 | const result = palindrome() 17 | 18 | expect(result).toBeUndefined() 19 | }) 20 | -------------------------------------------------------------------------------- /tests/suma_test.js: -------------------------------------------------------------------------------- 1 | const suma = (a, b) => { 2 | return a + b 3 | } 4 | 5 | const checks = [ 6 | { a: 0, b: 0, result: 0 }, 7 | { a: 1, b: 3, result: 4 }, 8 | { a: -3, b: 3, result: 0 } 9 | ] 10 | 11 | checks.forEach(check => { 12 | const { a, b, result } = check 13 | 14 | console.assert( 15 | suma(a, b) === result, 16 | `Suma of ${a} and ${b} expected to be ${result}` 17 | ) 18 | }) 19 | 20 | console.log(`${checks.length} checks performed...`) 21 | -------------------------------------------------------------------------------- /tests/user.test.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt') 2 | const User = require('../models/User') 3 | const { api, getUsers } = require('./helpers') 4 | const moongose = require('mongoose') 5 | const { server } = require('../index') 6 | 7 | describe('creating a new user', () => { 8 | beforeEach(async () => { 9 | await User.deleteMany({}) 10 | 11 | const passwordHash = await bcrypt.hash('pswd', 10) 12 | const user = new User({ username: 'miduroot', passwordHash }) 13 | 14 | await user.save() 15 | }) 16 | 17 | test('works as expected creating a fresh username', async () => { 18 | const usersAtStart = await getUsers() 19 | 20 | const newUser = { 21 | username: 'midudev', 22 | name: 'Miguel', 23 | password: 'tw1tch' 24 | } 25 | 26 | await api 27 | .post('/api/users') 28 | .send(newUser) 29 | .expect(201) 30 | .expect('Content-Type', /application\/json/) 31 | 32 | const usersAtEnd = await getUsers() 33 | 34 | expect(usersAtEnd).toHaveLength(usersAtStart.length + 1) 35 | 36 | const usernames = usersAtEnd.map(u => u.username) 37 | expect(usernames).toContain(newUser.username) 38 | }) 39 | 40 | test('creation fails with proper statuscode and message if username is already taken', async () => { 41 | const usersAtStart = await getUsers() 42 | 43 | const newUser = { 44 | username: 'miduroot', 45 | name: 'Miguel', 46 | password: 'midutest' 47 | } 48 | 49 | const result = await api 50 | .post('/api/users') 51 | .send(newUser) 52 | .expect(409) 53 | .expect('Content-Type', /application\/json/) 54 | 55 | console.log(result.body) 56 | 57 | expect(result.body.error).toContain('expected `username` to be unique') 58 | 59 | const usersAtEnd = await getUsers() 60 | expect(usersAtEnd).toHaveLength(usersAtStart.length) 61 | }) 62 | 63 | afterAll(() => { 64 | moongose.connection.close() 65 | server.close() 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /utils/for_testing.js: -------------------------------------------------------------------------------- 1 | const palindrome = (string) => { 2 | if (typeof string === 'undefined') return 3 | 4 | return string 5 | .split('') 6 | .reverse() 7 | .join('') 8 | } 9 | 10 | const average = array => { 11 | if (array.length === 0) return 0 12 | 13 | let sum = 0 14 | array.forEach(num => { sum += num }) 15 | return sum / array.length 16 | } 17 | 18 | module.exports = { 19 | palindrome, 20 | average 21 | } 22 | --------------------------------------------------------------------------------