diff --git a/models/Todo.js b/models/Todo.js index 4c2e67d..860a870 100644 --- a/models/Todo.js +++ b/models/Todo.js @@ -13,13 +13,15 @@ const TodoSchema = Schema({ }); TodoSchema.pre('save', async function () { - const user = await this.model('User').findById(this.user); - user.todos.push(this._id); - await user.save(); + if (this.isNew) { + 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(); + const list = await this.model('TodoList').findById(this.list); + list.todos.push(this._id); + await list.save(); + } }); TodoSchema.pre('remove', async function () { diff --git a/models/TodoList.js b/models/TodoList.js index e9ec914..428a6d7 100644 --- a/models/TodoList.js +++ b/models/TodoList.js @@ -12,16 +12,22 @@ const TodoListSchema = Schema({ }); TodoListSchema.pre('save', async function () { - const user = await this.model('User').findById(this.user); - user.lists.push(this._id); - await user.save(); + if (this.isNew) { + const user = await this.model('User').findById(this.user); + user.lists.push(this._id); + await user.save(); + } }); TodoListSchema.pre('remove', async function () { const user = await this.model('User').findById(this.user); - user.lists.splice(user.todos.indexOf(this._id), 1); + user.lists.splice(user.lists.indexOf(this._id), 1); await user.save(); - await this.model('Todo').remove({ list: this._id }); + + const todos = await this.model('Todo') + .find({ list: this._id }) + .exec(); + await Promise.all(todos.map(todo => todo.remove())); }); TodoListSchema.methods.toJson = function () { diff --git a/models/User.js b/models/User.js index db70e15..0d88f2d 100644 --- a/models/User.js +++ b/models/User.js @@ -22,8 +22,15 @@ 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 }); + await this.model('TodoList') + .find({ user: this._id }) + .remove() + .exec(); + + await this.model('Todo') + .find({ user: this._id }) + .remove() + .exec(); }); UserSchema.methods.generateJwt = function () { diff --git a/package-lock.json b/package-lock.json index d3192d9..03c2315 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2975,12 +2975,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2995,17 +2997,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3122,7 +3127,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3134,6 +3140,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3148,6 +3155,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3155,12 +3163,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3179,6 +3189,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3259,7 +3270,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3271,6 +3283,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3392,6 +3405,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/routes/lists.js b/routes/lists.js index f4dda10..22994b0 100644 --- a/routes/lists.js +++ b/routes/lists.js @@ -6,7 +6,6 @@ const router = express.Router(); const TodoList = mongoose.model('TodoList'); const asyncHelper = require('../asyncHelper'); -const auth = require('./auth'); const { NotFoundError } = require('../errors'); // index @@ -36,9 +35,7 @@ router.delete( '/:listId', asyncHelper(async (req, res) => { const { listId } = req.params; - const list = await TodoList.find({ _id: listId, user: req.user.id }) - .populate('todos') - .exec(); + const list = await TodoList.findOne({ _id: listId, user: req.user.id }).exec(); await list.remove(); res.json({ success: true }); }), @@ -50,7 +47,7 @@ router.patch( asyncHelper(async (req, res) => { const { listId } = req.params; const { name } = req.body; - const list = await TodoList.find({ _id: listId, user: req.user.id }); + const list = await TodoList.findOne({ _id: listId, user: req.user.id }); if (!list) { throw new NotFoundError("can't find list"); } diff --git a/routes/todos.js b/routes/todos.js index 422438b..6c4daf6 100644 --- a/routes/todos.js +++ b/routes/todos.js @@ -36,7 +36,7 @@ router.patch( asyncHelper(async (req, res) => { const { todoId } = req.params; const { text, completed } = req.body; - const todo = await Todo.find({ _id: todoId, user: req.user.id }); + const todo = await Todo.findOne({ _id: todoId, user: req.user.id }); if (!todo) { throw new NotFoundError("can't find todo"); } @@ -46,6 +46,7 @@ router.patch( if (completed !== undefined) { todo.completed = completed; } + await todo.save(); res.json({ success: true, data: todo.toJson() }); }), ); @@ -55,7 +56,7 @@ router.delete( '/:todoId', asyncHelper(async (req, res) => { const { todoId } = req.params; - const todo = await Todo.find({ _id: todoId, user: req.user.id }).exec(); + const todo = await Todo.findOne({ _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 c7ee4f6..86661d2 100644 --- a/routes/users.js +++ b/routes/users.js @@ -51,7 +51,8 @@ router.delete( '/user', auth.required, asyncHelper(async (req, res) => { - await User.findByIdAndRemove(req.user.id).exec(); + const user = await User.findById(req.user.id).exec(); + await user.remove(); res.json({ success: true }); }), ); diff --git a/tests/integration/lists.test.js b/tests/integration/lists.test.js index f4f1f03..84a0d27 100644 --- a/tests/integration/lists.test.js +++ b/tests/integration/lists.test.js @@ -40,12 +40,39 @@ afterAll(async () => { }); describe('test lists', () => { + test('should index lists', async () => { + const response = await request(server) + .get('/lists') + .set('Authorization', `Bearer ${token}`) + .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(response.body.data[0].name).toEqual('List1'); + }); test('should not index lists without authentication', async () => { await request(server) .get('/lists') .set('Accept', 'application/json') .expect(401); }); + test('should create list', async () => { + const response = await request(server) + .post('/lists') + .send({ + name: 'List2', + }) + .set('Authorization', `Bearer ${token}`) + .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(); + const freshUser = await User.findById(user.id).exec(); + expect(freshUser.lists).toContain(response.body.data.id); + }); test('should not create list without authentication', async () => { await request(server) .post('/lists') @@ -56,4 +83,56 @@ describe('test lists', () => { .set('Accept', 'application/json') .expect(401); }); + test('should update list', async () => { + const response = await request(server) + .patch(`/lists/${list._id}`) + .send({ + name: 'List2', + }) + .set('Authorization', `Bearer ${token}`) + .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(); + }); + test('should not update list without authentication', async () => { + await request(server) + .patch(`/lists/${list._id}`) + .send({ + name: 'List2', + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(401); + expect(await TodoList.findOne({ name: 'List2' })).toBeFalsy(); + }); + test('should remove list', async () => { + const response = await request(server) + .delete(`/lists/${list._id}`) + .set('Authorization', `Bearer ${token}`) + .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: 'List1' }).exec()).toBeFalsy(); + expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeFalsy(); + const freshUser = await User.findById(user.id).exec(); + expect(freshUser.lists).not.toContain(list._id); + expect(freshUser.todos).not.toContain(todo._id); + }); + test('should not remove list without authentication', async () => { + await request(server) + .delete(`/lists/${list._id}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(401); + expect(await TodoList.findOne({ name: 'List1' }).exec()).toBeTruthy(); + expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeTruthy(); + const freshUser = await User.findById(user.id).exec(); + expect(freshUser.lists).toContain(list._id); + expect(freshUser.todos).toContain(todo._id); + }); }); diff --git a/tests/integration/root.test.js b/tests/integration/root.test.js index ded8a49..5eed99c 100644 --- a/tests/integration/root.test.js +++ b/tests/integration/root.test.js @@ -1,43 +1,6 @@ const server = require('../../app.js'); const request = require('supertest'); -const mongoose = require('mongoose'); - -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'); - -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 () => { - ({ - user, token, list, todo, - } = await seed()); -}); - -afterEach(async () => { - await clean(); -}); - -afterAll(async () => { - await mongoose.disconnect(); - await mongoServer.stop(); - await server.close(); -}); describe('test not found', () => { test('respond not found with json', async () => { diff --git a/tests/integration/todos.test.js b/tests/integration/todos.test.js new file mode 100644 index 0000000..fecf5ca --- /dev/null +++ b/tests/integration/todos.test.js @@ -0,0 +1,137 @@ +const server = require('../../app.js'); + +const request = require('supertest'); +const mongoose = require('mongoose'); + +const Todo = mongoose.model('Todo'); +const User = mongoose.model('User'); + +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 () => { + ({ + user, token, list, todo, + } = await seed()); +}); + +afterEach(async () => { + await clean(); +}); + +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + await server.close(); +}); + +describe('test todos', () => { + test('should index todos', async () => { + const response = await request(server) + .get(`/lists/${list._id}/todos`) + .set('Authorization', `Bearer ${token}`) + .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(response.body.data[0].text).toEqual('Todo1'); + }); + test('should not index todos without authentication', async () => { + await request(server) + .get(`/lists/${list._id}/todos`) + .set('Accept', 'application/json') + .expect(401); + }); + test('should create todo', async () => { + const response = await request(server) + .post(`/lists/${list._id}/todos`) + .send({ + text: 'Todo2', + }) + .set('Authorization', `Bearer ${token}`) + .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 Todo.findOne({ text: 'Todo2', list: list._id })).toBeTruthy(); + const freshUser = await User.findById(user.id).exec(); + expect(freshUser.todos).toContain(response.body.data.id); + const freshList = await User.findById(user.id).exec(); + expect(freshList.todos).toContain(response.body.data.id); + }); + test('should not create todo without authentication', async () => { + await request(server) + .post(`/lists/${list._id}/todos`) + .send({ + text: 'Todo1', + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(401); + }); + test('should update todo', async () => { + const response = await request(server) + .patch(`/lists/${list._id}/todos/${todo._id}`) + .send({ + text: 'Todo2', + }) + .set('Authorization', `Bearer ${token}`) + .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 Todo.findOne({ text: 'Todo2' })).toBeTruthy(); + expect(await Todo.findOne({ text: 'Todo1' })).toBeFalsy(); + }); + test('should not update todo without authentication', async () => { + await request(server) + .patch(`/lists/${list._id}/todos/${todo._id}`) + .send({ + text: 'Todo2', + }) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(401); + expect(await Todo.findOne({ text: 'Todo1' })).toBeTruthy(); + expect(await Todo.findOne({ text: 'Todo2' })).toBeFalsy(); + }); + test('should remove todo', async () => { + const response = await request(server) + .delete(`/lists/${list._id}/todos/${todo._id}`) + .set('Authorization', `Bearer ${token}`) + .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 Todo.findOne({ text: 'Todo1' }).exec()).toBeFalsy(); + const freshUser = await User.findById(user.id).exec(); + expect(freshUser.todos).not.toContain(todo.id); + const freshList = await User.findById(user.id).exec(); + expect(freshList.todos).not.toContain(todo.id); + }); + test('should not remove todo without authentication', async () => { + await request(server) + .delete(`/lists/${list._id}/todos/${todo._id}`) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(401); + expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeTruthy(); + }); +}); diff --git a/tests/integration/users.test.js b/tests/integration/users.test.js index 96af242..01b88f8 100644 --- a/tests/integration/users.test.js +++ b/tests/integration/users.test.js @@ -13,10 +13,7 @@ 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 () => { @@ -26,9 +23,7 @@ beforeAll(async () => { }); beforeEach(async () => { - ({ - user, token, list, todo, - } = await seed()); + ({ token } = await seed()); }); afterEach(async () => { @@ -160,6 +155,8 @@ describe('test users', () => { .expect(401); expect(response.body.success).toBeFalsy(); expect(await User.findOne({ username: 'User1' }).exec()).toBeTruthy(); + expect(await TodoList.findOne({ name: 'List1' }).exec()).toBeTruthy(); + expect(await Todo.findOne({ text: 'Todo1' })).toBeTruthy(); }); test('should delete user', async () => { const response = await request(server) @@ -171,5 +168,7 @@ describe('test users', () => { .expect('Content-Type', 'application/json; charset=utf-8'); expect(response.body.success).toBeTruthy(); expect(await User.findOne({ username: 'User1' }).exec()).toBeFalsy(); + expect(await TodoList.findOne({ name: 'List1' }).exec()).toBeFalsy(); + expect(await Todo.findOne({ text: 'Todo1' }).exec()).toBeFalsy(); }); });