From db46f1a2b401fcdb13cbcbae5de82587f97df776 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Wed, 30 May 2018 21:42:30 +0300 Subject: [PATCH] require authentication for todos, use in-memody db for tests --- .vscode/settings.json | 3 +- app.js | 15 +- config/db.js | 6 +- config/index.js | 9 +- models/Todo.js | 15 +- models/TodoList.js | 15 +- models/User.js | 20 +- package-lock.json | 394 ++++++++++++++++++++++++++++++++ package.json | 2 + routes/lists.js | 22 +- routes/todos.js | 19 +- routes/users.js | 6 +- tests/integration/lists.test.js | 66 +++--- tests/integration/root.test.js | 33 ++- tests/integration/users.test.js | 61 +++-- tests/integration/utils.js | 33 +++ 16 files changed, 601 insertions(+), 118 deletions(-) create mode 100644 tests/integration/utils.js diff --git a/.vscode/settings.json b/.vscode/settings.json index e4fed00..49837d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,5 @@ "editor.tabSize": 2, "prettier.eslintIntegration": true, "editor.insertSpaces": true, -} \ No newline at end of file + "jest.pathToJest": "npm test --" +} diff --git a/app.js b/app.js index d40c065..147081f 100644 --- a/app.js +++ b/app.js @@ -6,24 +6,27 @@ const cors = require('cors'); const config = require('./config'); const db = require('./config/db'); +require('./models/TodoList'); +require('./models/User'); +require('./models/Todo'); + const app = express(); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); app.use(cors()); app.use(morgan('dev')); -require('./models/User'); -require('./models/TodoList'); -require('./models/Todo'); - const passport = require('./config/passport'); app.use(passport.initialize()); -app.use('/lists', require('./routes/lists')); -app.use('/todos', require('./routes/todos')); app.use('/users', require('./routes/users')); +const auth = require('./routes/auth'); + +app.use('/lists', auth.required, require('./routes/lists')); +app.use('/todos', auth.required, require('./routes/todos')); + // 404 route app.use((req, res) => { res.status(404); diff --git a/config/db.js b/config/db.js index 471a063..17f6458 100644 --- a/config/db.js +++ b/config/db.js @@ -1,10 +1,10 @@ const mongoose = require('mongoose'); const config = require('./'); -const { host, port, name } = config.db; -const connectionString = `mongodb://${host}:${port}/${name}`; - async function connect() { + const { host, port, name } = config.db; + const connectionString = `mongodb://${host}:${port}/${name}`; + await mongoose.connect(connectionString); } diff --git a/config/index.js b/config/index.js index 5f13842..fc334bf 100644 --- a/config/index.js +++ b/config/index.js @@ -11,15 +11,8 @@ const dev = { }, secret: process.env.DEV_SECRET || 'devsecret', }; + const test = { - app: { - port: process.env.TEST_APP_PORT || 4001, - }, - db: { - host: process.env.TEST_DB_HOST || 'localhost', - port: process.env.TEST_DB_PORT || 27017, - name: process.env.TEST_DB_NAME || 'todolistTest', - }, secret: process.env.TEST_SECRET || 'testsecret', }; diff --git a/models/Todo.js b/models/Todo.js index f439f2c..4c2e67d 100644 --- a/models/Todo.js +++ b/models/Todo.js @@ -1,6 +1,5 @@ const mongoose = require('mongoose'); -const TodoList = mongoose.model('TodoList'); const { Schema } = mongoose; const TodoSchema = Schema({ @@ -9,17 +8,26 @@ const TodoSchema = Schema({ required: true, }, list: { type: Schema.Types.ObjectId, ref: 'TodoList', required: true }, + user: { type: Schema.Types.ObjectId, ref: 'User', required: true }, completed: { type: Boolean, default: false }, }); TodoSchema.pre('save', async function () { - const list = await TodoList.findById(this.list); + const user = await this.model('User').findById(this.user); + user.todos.push(this._id); + await user.save(); + + const list = await this.model('TodoList').findById(this.list); list.todos.push(this._id); await list.save(); }); TodoSchema.pre('remove', async function () { - const list = await TodoList.findById(this.list); + const user = await this.model('User').findById(this.user); + user.todos.splice(user.todos.indexOf(this._id), 1); + await user.save(); + + const list = await this.model('TodoList').findById(this.list); list.todos.splice(list.todos.indexOf(this._id), 1); await list.save(); }); @@ -29,6 +37,7 @@ TodoSchema.methods.toJson = function () { id: this._id.toString(), text: this.text, list: this.list.toString(), + user: this.user.toString(), completed: this.completed, }; }; diff --git a/models/TodoList.js b/models/TodoList.js index 644a1a2..e9ec914 100644 --- a/models/TodoList.js +++ b/models/TodoList.js @@ -8,18 +8,27 @@ const TodoListSchema = Schema({ required: true, }, todos: [{ type: Schema.Types.ObjectId, ref: 'Todo' }], + user: { type: Schema.Types.ObjectId, ref: 'User', required: true }, +}); + +TodoListSchema.pre('save', async function () { + const user = await this.model('User').findById(this.user); + user.lists.push(this._id); + await user.save(); }); TodoListSchema.pre('remove', async function () { - this.todos.forEach(async (todo) => { - await todo.remove(); - }); + const user = await this.model('User').findById(this.user); + user.lists.splice(user.todos.indexOf(this._id), 1); + await user.save(); + await this.model('Todo').remove({ list: this._id }); }); TodoListSchema.methods.toJson = function () { const todos = this.populated('todos') ? this.todos.map(todo => todo.toJson()) : this.todos; return { id: this._id.toString(), + user: this.user.toString(), name: this.name, todos, }; diff --git a/models/User.js b/models/User.js index 5ee9f7a..db70e15 100644 --- a/models/User.js +++ b/models/User.js @@ -1,17 +1,33 @@ const mongoose = require('mongoose'); const passportLocalMongoose = require('passport-local-mongoose'); const jwt = require('jsonwebtoken'); +const uniqueValidator = require('mongoose-unique-validator'); const { secret } = require('../config'); const { Schema } = mongoose; -const UserSchema = Schema({ username: { type: String, required: true } }); +const UserSchema = Schema({ + username: { + type: String, + required: true, + unique: true, + validate: /^\S*$/, + }, + lists: [{ type: Schema.Types.ObjectId, ref: 'TodoList' }], + todos: [{ type: Schema.Types.ObjectId, ref: 'Todo' }], +}); UserSchema.plugin(passportLocalMongoose); +UserSchema.plugin(uniqueValidator); + +UserSchema.pre('remove', async function () { + await this.model('TodoList').remove({ user: this._id }); + await this.model('Todo').remove({ user: this._id }); +}); UserSchema.methods.generateJwt = function () { - return jwt.sign({ id: this._id, username: this.username }, secret, { expiresIn: '1y' }); + return jwt.sign({ id: this._id, username: this.username }, secret, { expiresIn: '6m' }); }; UserSchema.methods.toAuthJson = function () { diff --git a/package-lock.json b/package-lock.json index d6be900..d3192d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1033,6 +1033,12 @@ } } }, + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=", + "dev": true + }, "basic-auth": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", @@ -1057,6 +1063,16 @@ "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, + "bl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "bluebird": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", @@ -1169,11 +1185,50 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz", "integrity": "sha512-D8zmlb46xfuK2gGvKmUjIklQEouN2nQ0LEHHeZ/NoHM2LDiMk2EYzZ5Ntw/Urk+bgMDosOZxaRzXxvhI5TcAVQ==" }, + "buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", + "dev": true, + "requires": { + "base64-js": "0.0.8", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "dev": true + }, "buffer-from": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz", @@ -1554,6 +1609,15 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, "compare-versions": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.2.1.tgz", @@ -1764,6 +1828,95 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", + "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", + "dev": true, + "requires": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + } + }, + "decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "requires": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + } + }, + "decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "dependencies": { + "file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true + } + } + }, + "decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "requires": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "requires": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "dependencies": { + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + } + } + }, "deep-extend": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", @@ -1983,6 +2136,15 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", @@ -2574,6 +2736,15 @@ "bser": "^2.0.0" } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -2593,6 +2764,12 @@ "object-assign": "^4.0.1" } }, + "file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", + "dev": true + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -2734,6 +2911,23 @@ "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", "dev": true }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3292,6 +3486,12 @@ "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", "dev": true }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", + "dev": true + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -3304,6 +3504,26 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, + "getos": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.1.0.tgz", + "integrity": "sha512-i9vrxtDu5DlLVFcrbqUqGWYlZN/zZ4pGMICCAcZoYsX3JA54nYp8r5EThw5K+m2q3wszkx4Th746JstspB0H4Q==", + "dev": true, + "requires": { + "async": "2.4.0" + }, + "dependencies": { + "async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.4.0.tgz", + "integrity": "sha1-SZAgDxjqW4N8LMT4wDGmmFw4VhE=", + "dev": true, + "requires": { + "lodash": "^4.14.0" + } + } + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -3400,6 +3620,12 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -3608,6 +3834,12 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz", + "integrity": "sha512-VhDzCKN7K8ufStx/CLj5/PDTMgph+qwN5Pkd5i0sGnVwk56zJ0lkT8Qzi1xqWLS0Wp29DgDtNeS7v8/wMoZeHg==", + "dev": true + }, "ignore": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", @@ -3880,6 +4112,12 @@ "is-path-inside": "^1.0.0" } }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, "is-npm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", @@ -4653,6 +4891,15 @@ "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -4800,11 +5047,25 @@ } } }, + "lockfile": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz", + "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==", + "dev": true, + "requires": { + "signal-exit": "^3.0.2" + } + }, "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -4940,6 +5201,15 @@ "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", "dev": true }, + "md5-file": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-3.2.3.tgz", + "integrity": "sha512-3Tkp1piAHaworfcCgH0jKbTvj1jWWFgbvh2cXaNCgHwyTCBxxvD1Y04rmfpvdPm1P4oXMOpm6+2H7sr7v9v8Fw==", + "dev": true, + "requires": { + "buffer-alloc": "^1.1.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -5086,6 +5356,38 @@ "require_optional": "^1.0.1" } }, + "mongodb-memory-server": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-1.7.4.tgz", + "integrity": "sha512-8CNbBV80cBradJEuyAl+Vw45Wf0MJYTplDphGE9z3qqaQVRYG25PhZ2Uk+kyVVF6HqF4obe+meCMpoOoeXwJ8w==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "debug": "^3.1.0", + "decompress": "^4.2.0", + "fs-extra": "^5.0.0", + "get-port": "^3.2.0", + "getos": "^3.1.0", + "lockfile": "^1.0.4", + "md5-file": "^3.2.3", + "mkdirp": "^0.5.1", + "request": "^2.85.0", + "request-promise": "^4.2.2", + "tmp": "^0.0.33", + "uuid": "^3.2.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "mongoose": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.1.2.tgz", @@ -5116,6 +5418,15 @@ "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" }, + "mongoose-unique-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mongoose-unique-validator/-/mongoose-unique-validator-2.0.1.tgz", + "integrity": "sha512-Eqq7lZMy0nPSojG8UyDZvlBie1aBZJXk68GDMBXXQH0TAi0hZHf76nCrwuipReNK1jLkjyKzV7eIZotja5eEBw==", + "requires": { + "lodash.foreach": "^4.1.0", + "lodash.get": "^4.0.2" + } + }, "morgan": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", @@ -5697,6 +6008,12 @@ "through": "~2.3" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -6098,6 +6415,18 @@ "uuid": "^3.1.0" } }, + "request-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", + "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + } + }, "request-promise-core": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", @@ -6588,6 +6917,15 @@ "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.0.0.tgz", "integrity": "sha1-JHEQ7yLM+JexOj8KvdtSeCOTzWo=" }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "dev": true, + "requires": { + "commander": "~2.8.1" + } + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -7058,6 +7396,15 @@ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, + "strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "requires": { + "is-natural-number": "^4.0.1" + } + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -7135,6 +7482,21 @@ "string-width": "^2.1.1" } }, + "tar-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", + "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", + "dev": true, + "requires": { + "bl": "^1.0.0", + "buffer-alloc": "^1.1.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.0", + "xtend": "^4.0.0" + } + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -7526,6 +7888,12 @@ "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", "dev": true }, + "to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -7689,6 +8057,16 @@ "dev": true, "optional": true }, + "unbzip2-stream": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", + "integrity": "sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==", + "dev": true, + "requires": { + "buffer": "^3.0.1", + "through": "^2.3.6" + } + }, "undefsafe": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", @@ -7742,6 +8120,12 @@ "crypto-random-string": "^1.0.0" } }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8185,6 +8569,16 @@ "dev": true } } + }, + "yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.0.1" + } } } } diff --git a/package.json b/package.json index 1503fa0..f961e98 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "express-jwt": "^5.3.1", "jsonwebtoken": "^8.2.1", "mongoose": "^5.1.1", + "mongoose-unique-validator": "^2.0.1", "morgan": "^1.9.0", "passport": "^0.4.0", "passport-local": "^1.0.0", @@ -30,6 +31,7 @@ "eslint-plugin-import": "^2.12.0", "eslint-plugin-jest": "^21.15.2", "jest": "^22.4.4", + "mongodb-memory-server": "^1.7.4", "nodemon": "^1.17.5", "supertest": "^3.1.0" }, diff --git a/routes/lists.js b/routes/lists.js index 21633cf..f4dda10 100644 --- a/routes/lists.js +++ b/routes/lists.js @@ -6,12 +6,14 @@ const router = express.Router(); const TodoList = mongoose.model('TodoList'); const asyncHelper = require('../asyncHelper'); +const auth = require('./auth'); +const { NotFoundError } = require('../errors'); // index router.get( '/', asyncHelper(async (req, res) => { - const lists = await TodoList.find({}) + const lists = await TodoList.find({ user: req.user.id }) .populate('todos') .exec(); res.json({ success: true, data: lists.map(list => list.toJson()) }); @@ -23,7 +25,7 @@ router.post( '/', asyncHelper(async (req, res) => { const { name } = req.body; - const newList = new TodoList({ name }); + const newList = new TodoList({ name, user: req.user.id }); await newList.save(); res.json({ success: true, data: newList.toJson() }); }), @@ -34,7 +36,7 @@ router.delete( '/:listId', asyncHelper(async (req, res) => { const { listId } = req.params; - const list = await TodoList.findById(listId) + const list = await TodoList.find({ _id: listId, user: req.user.id }) .populate('todos') .exec(); await list.remove(); @@ -48,15 +50,13 @@ router.patch( asyncHelper(async (req, res) => { const { listId } = req.params; const { name } = req.body; - const patch = {}; - if (name !== undefined) { - patch.name = name; + const list = await TodoList.find({ _id: listId, user: req.user.id }); + if (!list) { + throw new NotFoundError("can't find list"); + } + if (name !== undefined) { + list.name = name; } - const list = await TodoList.findByIdAndUpdate( - { _id: listId }, - { $set: patch }, - { new: true }, - ).exec(); await list.save(); res.json({ success: true, data: list.toJson() }); }), diff --git a/routes/todos.js b/routes/todos.js index 3a2d555..422438b 100644 --- a/routes/todos.js +++ b/routes/todos.js @@ -13,7 +13,7 @@ router.get( '/', asyncHelper(async (req, res) => { const { listId } = res.locals || req.body; - const todos = await Todo.find({ list: listId }).exec(); + const todos = await Todo.find({ list: listId, user: req.user.id }).exec(); res.json({ success: true, data: todos.map(todo => todo.toJson()) }); }), ); @@ -24,7 +24,7 @@ router.post( asyncHelper(async (req, res) => { const { listId } = res.locals || req.body; const { text } = req.body; - const todo = new Todo({ text, list: listId }); + const todo = new Todo({ text, list: listId, user: req.user.id }); await todo.save(); res.json({ success: true, data: todo.toJson() }); }), @@ -36,16 +36,15 @@ router.patch( asyncHelper(async (req, res) => { const { todoId } = req.params; const { text, completed } = req.body; - const patch = {}; + const todo = await Todo.find({ _id: todoId, user: req.user.id }); + if (!todo) { + throw new NotFoundError("can't find todo"); + } if (text !== undefined) { - patch.text = text; + todo.text = text; } if (completed !== undefined) { - patch.completed = completed; - } - const todo = await Todo.findByIdAndUpdate(todoId, { $set: patch }, { new: true }).exec(); - if (!todo) { - throw new NotFoundError(`can't find todo with id ${todoId}`); + todo.completed = completed; } res.json({ success: true, data: todo.toJson() }); }), @@ -56,7 +55,7 @@ router.delete( '/:todoId', asyncHelper(async (req, res) => { const { todoId } = req.params; - const todo = await Todo.findById(todoId).exec(); + const todo = await Todo.find({ _id: todoId, user: req.user.id }).exec(); if (!todo) { throw new NotFoundError(`can't find todo with id ${todoId}`); } diff --git a/routes/users.js b/routes/users.js index c6e8d17..c7ee4f6 100644 --- a/routes/users.js +++ b/routes/users.js @@ -31,7 +31,11 @@ router.patch( if (username !== undefined) { patch.username = username; } - const user = await User.findByIdAndUpdate(req.user.id, { $set: patch }, { new: true }).exec(); + const user = await User.findOneAndUpdate( + { _id: req.user.id }, + { $set: patch }, + { runValidators: true, context: 'query', new: true }, + ).exec(); if (!user) { throw new NotFoundError(`can't find user with username ${req.user.username}`); } diff --git a/tests/integration/lists.test.js b/tests/integration/lists.test.js index 8574d64..f4f1f03 100644 --- a/tests/integration/lists.test.js +++ b/tests/integration/lists.test.js @@ -3,65 +3,57 @@ const server = require('../../app.js'); const request = require('supertest'); const mongoose = require('mongoose'); -const db = require('../../config/db'); - -const TodoList = mongoose.model('TodoList'); const Todo = mongoose.model('Todo'); +const TodoList = mongoose.model('TodoList'); +const User = mongoose.model('User'); -let lists; -let listsPopulated; -let todos; +jest.setTimeout(60000); +const MongoDBMemoryServer = require('mongodb-memory-server').default; +const { seed, clean } = require('./utils'); + +let user; +let token; +let list; +let todo; +let mongoServer; + +beforeAll(async () => { + mongoServer = new MongoDBMemoryServer(); + const mongoUri = await mongoServer.getConnectionString(); + await mongoose.connect(mongoUri); +}); beforeEach(async () => { - await db.connect(); - - // seed lists and todos - const list1 = new TodoList({ name: 'List1' }); - const todo1 = new Todo({ text: 'Todo1', list: list1._id }); - const todo2 = new Todo({ text: 'Todo2', list: list1._id }); - - await list1.save(); - await todo1.save(); - await todo2.save(); - lists = await TodoList.find({}).exec(); - listsPopulated = await TodoList.find({}) - .populate('todos') - .exec(); - todos = await Todo.find({}).exec(); + ({ + user, token, list, todo, + } = await seed()); }); afterEach(async () => { - await TodoList.remove({}).exec(); - await Todo.remove({}).exec(); - await db.disconnect(); + await clean(); }); afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); await server.close(); }); describe('test lists', () => { - test('should index lists', async () => { - const response = await request(server) + test('should not index lists without authentication', async () => { + await request(server) .get('/lists') .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', 'application/json; charset=utf-8'); - expect(response.body.success).toBe(true); - expect(response.body.data).toBeInstanceOf(Array); - expect(response.body.data).toEqual(listsPopulated.map(list => list.toJson())); + .expect(401); }); - test('should create list', async () => { - const response = await request(server) + test('should not create list without authentication', async () => { + await request(server) .post('/lists') .send({ name: 'List2', }) .set('Content-Type', 'application/json') .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', 'application/json; charset=utf-8'); - expect(response.body.success).toBeTruthy(); - expect(await TodoList.findOne({ name: 'List2' })).toBeTruthy(); + .expect(401); }); }); diff --git a/tests/integration/root.test.js b/tests/integration/root.test.js index a5b0def..ded8a49 100644 --- a/tests/integration/root.test.js +++ b/tests/integration/root.test.js @@ -3,26 +3,43 @@ const server = require('../../app.js'); const request = require('supertest'); const mongoose = require('mongoose'); -const Todo = require('../../models/Todo'); -const TodoList = require('../../models/TodoList'); +const Todo = mongoose.model('Todo'); +const TodoList = mongoose.model('TodoList'); +const User = mongoose.model('User'); -const db = require('../../config/db'); +jest.setTimeout(60000); +const MongoDBMemoryServer = require('mongodb-memory-server').default; +const { seed, clean } = require('./utils'); + +let user; +let token; +let list; +let todo; +let mongoServer; + +beforeAll(async () => { + mongoServer = new MongoDBMemoryServer(); + const mongoUri = await mongoServer.getConnectionString(); + await mongoose.connect(mongoUri); +}); beforeEach(async () => { - await db.connect(); + ({ + user, token, list, todo, + } = await seed()); }); afterEach(async () => { - await db.disconnect(); + await clean(); }); afterAll(async () => { - await TodoList.remove({}).exec(); - await Todo.remove({}).exec(); + await mongoose.disconnect(); + await mongoServer.stop(); await server.close(); }); -describe('Test not found', () => { +describe('test not found', () => { test('respond not found with json', async () => { const response = await request(server) .get('/') diff --git a/tests/integration/users.test.js b/tests/integration/users.test.js index 781d021..96af242 100644 --- a/tests/integration/users.test.js +++ b/tests/integration/users.test.js @@ -4,29 +4,40 @@ const request = require('supertest'); const mongoose = require('mongoose'); const jwt = require('jsonwebtoken'); -const db = require('../../config/db'); -const { secret } = require('../../config'); - +const Todo = mongoose.model('Todo'); +const TodoList = mongoose.model('TodoList'); const User = mongoose.model('User'); +jest.setTimeout(60000); +const MongoDBMemoryServer = require('mongodb-memory-server').default; +const { seed, clean } = require('./utils'); +const { secret } = require('../../config'); + let user; let token; +let list; +let todo; +let mongoServer; + +beforeAll(async () => { + mongoServer = new MongoDBMemoryServer(); + const mongoUri = await mongoServer.getConnectionString(); + await mongoose.connect(mongoUri); +}); beforeEach(async () => { - await db.connect(); - - user = new User({ username: 'User' }); - await user.setPassword('password'); - await user.save(); - token = user.generateJwt(); + ({ + user, token, list, todo, + } = await seed()); }); afterEach(async () => { - await User.remove({}).exec(); - await db.disconnect(); + await clean(); }); afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); await server.close(); }); @@ -35,8 +46,8 @@ describe('test users', () => { const response = await request(server) .post('/users') .send({ - username: 'User 2', - password: 'password 2', + username: 'User2', + password: 'password2', }) .set('Content-Type', 'application/json') .set('Accept', 'application/json') @@ -44,8 +55,8 @@ describe('test users', () => { .expect('Content-Type', 'application/json; charset=utf-8'); expect(response.body.success).toBeTruthy(); const tokenDecoded = jwt.verify(response.body.data.jwt, secret); - expect(tokenDecoded.username).toEqual('User 2'); - const userAuth = await User.authenticate()('User 2', 'password 2'); + expect(tokenDecoded.username).toEqual('User2'); + const userAuth = await User.authenticate()('User2', 'password2'); expect(userAuth.user).toBeTruthy(); }); test('should not create user with no username', async () => { @@ -53,7 +64,7 @@ describe('test users', () => { .post('/users') .send({ username: '', - password: 'password 2', + password: 'password2', }) .set('Content-Type', 'application/json') .set('Accept', 'application/json') @@ -78,8 +89,8 @@ describe('test users', () => { const response = await request(server) .post('/users/login') .send({ - username: 'User', - password: 'password', + username: 'User1', + password: 'password1', }) .set('Content-Type', 'application/json') .set('Accept', 'application/json') @@ -87,7 +98,7 @@ describe('test users', () => { .expect('Content-Type', 'application/json; charset=utf-8'); expect(response.body.success).toBeTruthy(); const tokenDecoded = jwt.verify(response.body.data.jwt, secret); - expect(tokenDecoded.username).toEqual('User'); + expect(tokenDecoded.username).toEqual('User1'); }); test('should not login user with no name', async () => { await request(server) @@ -115,7 +126,7 @@ describe('test users', () => { const response = await request(server) .patch('/users/user') .send({ - username: 'User 2', + username: 'User2', password: 'password2', }) .set('Authorization', `Bearer ${token}`) @@ -125,15 +136,15 @@ describe('test users', () => { .expect('Content-Type', 'application/json; charset=utf-8'); expect(response.body.success).toBeTruthy(); const tokenDecoded = jwt.verify(response.body.data.jwt, secret); - expect(tokenDecoded.username).toEqual('User 2'); - const userAuth = await User.authenticate()('User 2', 'password2'); + expect(tokenDecoded.username).toEqual('User2'); + const userAuth = await User.authenticate()('User2', 'password2'); expect(userAuth.user).toBeTruthy(); }); test('should not update user without authentication', async () => { const response = await request(server) .patch('/users/user') .send({ - username: 'User 2', + username: 'User2', password: 'password2', }) .set('Content-Type', 'application/json') @@ -148,7 +159,7 @@ describe('test users', () => { .set('Accept', 'application/json') .expect(401); expect(response.body.success).toBeFalsy(); - expect(await User.findOne({ username: 'User' }).exec()).toBeTruthy(); + expect(await User.findOne({ username: 'User1' }).exec()).toBeTruthy(); }); test('should delete user', async () => { const response = await request(server) @@ -159,6 +170,6 @@ describe('test users', () => { .expect(200) .expect('Content-Type', 'application/json; charset=utf-8'); expect(response.body.success).toBeTruthy(); - expect(await User.findOne({ username: 'User' }).exec()).toBeFalsy(); + expect(await User.findOne({ username: 'User1' }).exec()).toBeFalsy(); }); }); diff --git a/tests/integration/utils.js b/tests/integration/utils.js new file mode 100644 index 0000000..6e2e36d --- /dev/null +++ b/tests/integration/utils.js @@ -0,0 +1,33 @@ +const mongoose = require('mongoose'); + +const User = mongoose.model('User'); +const Todo = mongoose.model('Todo'); +const TodoList = mongoose.model('TodoList'); + +async function seed() { + const user = new User({ username: 'User1' }); + await user.setPassword('password1'); + await user.save(); + const token = user.generateJwt(); + + const list = new TodoList({ name: 'List1', user: user._id }); + const todo = new Todo({ text: 'Todo1', list: list._id, user: user._id }); + + await list.save(); + await todo.save(); + + return { + user, + token, + list, + todo, + }; +} + +async function clean() { + await TodoList.remove({}).exec(); + await Todo.remove({}).exec(); + await User.remove({}).exec(); +} + +module.exports = { seed, clean };