From 34fdd9cb5cde3e4f92ca3b76a5b31a367fc73b0f Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Thu, 9 Jul 2020 18:00:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(subscriptions):=20init=20mail=20subscripti?= =?UTF-8?q?ons=20example=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +- .../controllers/subscriptions.controller.js | 97 +++++ .../models/subscriptions.model.mongoose.js | 39 ++ .../models/subscriptions.schema.js | 16 + .../policies/subscriptions.policy.js | 31 ++ .../repositories/subscriptions.repository.js | 72 ++++ .../routes/subscriptions.routes.js | 29 ++ .../services/subscriptions.service.js | 55 +++ .../tests/subscriptions.crud.tests.js | 351 ++++++++++++++++++ .../tests/subscriptions.schema.tests.js | 66 ++++ modules/tasks/tests/tasks.crud.tests.js | 2 +- 11 files changed, 765 insertions(+), 5 deletions(-) create mode 100644 modules/subscriptions/controllers/subscriptions.controller.js create mode 100644 modules/subscriptions/models/subscriptions.model.mongoose.js create mode 100644 modules/subscriptions/models/subscriptions.schema.js create mode 100644 modules/subscriptions/policies/subscriptions.policy.js create mode 100644 modules/subscriptions/repositories/subscriptions.repository.js create mode 100644 modules/subscriptions/routes/subscriptions.routes.js create mode 100644 modules/subscriptions/services/subscriptions.service.js create mode 100644 modules/subscriptions/tests/subscriptions.crud.tests.js create mode 100644 modules/subscriptions/tests/subscriptions.schema.tests.js diff --git a/README.md b/README.md index f65ea4a09..67414c796 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,17 @@ Our stack node is actually in Beta. ## :tada: Features Overview -#### Available +### Core * **User** : classic register / auth or oAuth(microsoft, google) - profile management (update, avatar upload ...) -* **User data privacy** : delete all data - get all data - send all data by mail +* **User data privacy** : delete all - get all - send all by mail * **Admin** : list users - get user - edit user - delete user -* **Tasks** : list tasks - get task - add tasks - edit tasks - delete tasks -* **Uploads** : get upload stream - add upload - delete upload - get image upload stream & sharp operations + +### Examples + +* **Tasks** : list - get - add - edit - delete +* **Files Uploads** : get stream - add - delete - get image stream & sharp operations +* **Mails Subscriptions** : list - get - add - edit - delete ## :pushpin: Prerequisites diff --git a/modules/subscriptions/controllers/subscriptions.controller.js b/modules/subscriptions/controllers/subscriptions.controller.js new file mode 100644 index 000000000..709870ed4 --- /dev/null +++ b/modules/subscriptions/controllers/subscriptions.controller.js @@ -0,0 +1,97 @@ +/** + * Module dependencies + */ +const path = require('path'); + +const errors = require(path.resolve('./lib/helpers/errors')); +const responses = require(path.resolve('./lib/helpers/responses')); + +const SubscriptionsService = require('../services/subscriptions.service'); + +/** + * @desc Endpoint to ask the service to get the list of subscriptions + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +exports.list = async (req, res) => { + try { + const subscriptions = await SubscriptionsService.list(); + responses.success(res, 'subscription list')(subscriptions); + } catch (err) { + responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err); + } +}; + +/** + * @desc Endpoint to ask the service to create a subscription + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +exports.create = async (req, res) => { + try { + const subscription = await SubscriptionsService.create(req.body); + responses.success(res, 'subscription created')(subscription); + } catch (err) { + responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err); + } +}; + +/** + * @desc Endpoint to show the current subscription + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +exports.get = (req, res) => { + const subscription = req.subscription ? req.subscription.toJSON() : {}; + responses.success(res, 'subscription get')(subscription); +}; + +/** + * @desc Endpoint to ask the service to update a subscription + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +exports.update = async (req, res) => { + try { + const subscription = await SubscriptionsService.update(req.subscription, req.body); + responses.success(res, 'subscription updated')(subscription); + } catch (err) { + responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err); + } +}; + +/** + * @desc Endpoint to ask the service to delete a subscription + * @param {Object} req - Express request object + * @param {Object} res - Express response object + */ +exports.delete = async (req, res) => { + try { + const result = await SubscriptionsService.delete(req.subscription); + result.id = req.subscription.id; + responses.success(res, 'subscription deleted')(result); + } catch (err) { + responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err); + } +}; + +/** + * @desc MiddleWare to ask the service the subscription for this id + * @param {Object} req - Express request object + * @param {Object} res - Express response object + * @param {Function} next - Express next middleware function + * @param {String} id - subscription id + */ +exports.subscriptionByID = async (req, res, next, id) => { + try { + const subscription = await SubscriptionsService.get(id); + if (!subscription) responses.error(res, 404, 'Not Found', 'No Subscription with that identifier has been found')(); + else { + req.subscription = subscription; + // if (subscription.user) req.isOwner = subscription.user._id; // user id used if we proteck road by isOwner policy + next(); + } + } catch (err) { + next(err); + } +}; diff --git a/modules/subscriptions/models/subscriptions.model.mongoose.js b/modules/subscriptions/models/subscriptions.model.mongoose.js new file mode 100644 index 000000000..c4ca1c73a --- /dev/null +++ b/modules/subscriptions/models/subscriptions.model.mongoose.js @@ -0,0 +1,39 @@ +/** + * Module dependencies + */ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +/** + * Data Model Mongoose + */ +const SubscriptionMongoose = new Schema({ + email: { + type: String, + unique: 'Email already exists', + }, + news: Boolean, +}, { + timestamps: true, +}); + +/** + * @desc Function to add id (+ _id) to all objects + * @param {Object} subscription + * @return {Object} Subscription + */ +function addID() { + return this._id.toHexString(); +} + +/** + * Model configuration + */ +SubscriptionMongoose.virtual('id').get(addID); +// Ensure virtual fields are serialised. +SubscriptionMongoose.set('toJSON', { + virtuals: true, +}); + +mongoose.model('Subscription', SubscriptionMongoose); diff --git a/modules/subscriptions/models/subscriptions.schema.js b/modules/subscriptions/models/subscriptions.schema.js new file mode 100644 index 000000000..ab5e40564 --- /dev/null +++ b/modules/subscriptions/models/subscriptions.schema.js @@ -0,0 +1,16 @@ +/** + * Module dependencies + */ +const Joi = require('@hapi/joi'); + +/** + * Data Schema + */ +const SubscriptionSchema = Joi.object().keys({ + email: Joi.string().email().required(), + news: Joi.boolean().default(true).required(), +}); + +module.exports = { + Subscription: SubscriptionSchema, +}; diff --git a/modules/subscriptions/policies/subscriptions.policy.js b/modules/subscriptions/policies/subscriptions.policy.js new file mode 100644 index 000000000..e737bc793 --- /dev/null +++ b/modules/subscriptions/policies/subscriptions.policy.js @@ -0,0 +1,31 @@ +/** + * Module dependencies +* */ +const path = require('path'); + +const policy = require(path.resolve('./lib/middlewares/policy')); + +/** + * Invoke Subscriptions Permissions + */ +exports.invokeRolesPolicies = () => { + policy.Acl.allow([{ + roles: ['guest'], + allows: [{ + resources: '/api/subscriptions', + permissions: ['post'], + }, { + resources: '/api/subscriptions/:subscriptionId', + permissions: ['get', 'put', 'delete'], + }], + }, { + roles: ['admin'], + allows: [{ + resources: '/api/subscriptions', + permissions: '*', + }, { + resources: '/api/subscriptions/:subscriptionId', + permissions: '*', + }], + }]); +}; diff --git a/modules/subscriptions/repositories/subscriptions.repository.js b/modules/subscriptions/repositories/subscriptions.repository.js new file mode 100644 index 000000000..332063f78 --- /dev/null +++ b/modules/subscriptions/repositories/subscriptions.repository.js @@ -0,0 +1,72 @@ +/** + * Module dependencies + */ +const mongoose = require('mongoose'); + +const Subscription = mongoose.model('Subscription'); + +/** + * @desc Function to get all subscription in db with filter or not + * @return {Array} subscriptions + */ +exports.list = (filter) => Subscription.find(filter).sort('-createdAt').exec(); + +/** + * @desc Function to create a subscription in db + * @param {Object} subscription + * @return {Object} subscription + */ +exports.create = (subscription) => new Subscription(subscription).save(); + +/** + * @desc Function to get a subscription from db + * @param {String} id + * @return {Object} subscription + */ +exports.get = (id) => { + if (!mongoose.Types.ObjectId.isValid(id)) return null; + return Subscription.findOne({ _id: id }).exec(); +}; + +/** + * @desc Function to update a subscription in db + * @param {Object} subscription + * @return {Object} subscription + */ +exports.update = (subscription) => new Subscription(subscription).save(); + +/** + * @desc Function to delete a subscription in db + * @param {Object} subscription + * @return {Object} confirmation of delete + */ +exports.delete = (subscription) => Subscription.deleteOne({ _id: subscription.id }).exec(); + +/** + * @desc Function to delete subscriptions of one user in db + * @param {Object} filter + * @return {Object} confirmation of delete + */ +exports.deleteMany = (filter) => { + if (filter) return Subscription.deleteMany(filter).exec(); +}; + +/** + * @desc Function to import list of subscriptions in db + * @param {[Object]} subscriptions + * @param {[String]} filters + * @return {Object} subscriptions + */ +exports.import = (subscriptions, filters) => Subscription.bulkWrite(subscriptions.map((subscription) => { + const filter = {}; + filters.forEach((value) => { + filter[value] = subscription[value]; + }); + return { + updateOne: { + filter, + update: subscription, + upsert: true, + }, + }; +})); diff --git a/modules/subscriptions/routes/subscriptions.routes.js b/modules/subscriptions/routes/subscriptions.routes.js new file mode 100644 index 000000000..616323488 --- /dev/null +++ b/modules/subscriptions/routes/subscriptions.routes.js @@ -0,0 +1,29 @@ +/** + * Module dependencies + */ +const passport = require('passport'); +const path = require('path'); + +const model = require(path.resolve('./lib/middlewares/model')); +const policy = require(path.resolve('./lib/middlewares/policy')); +const subscriptions = require('../controllers/subscriptions.controller'); +const subscriptionsSchema = require('../models/subscriptions.schema'); + +/** + * Routes + */ +module.exports = (app) => { + // list & post + app.route('/api/subscriptions') + .get(passport.authenticate('jwt'), policy.isAllowed, subscriptions.list) // list + .post(policy.isAllowed, model.isValid(subscriptionsSchema.Subscription), subscriptions.create); // create + + // classic crud + app.route('/api/subscriptions/:subscriptionId').all(policy.isAllowed) // policy.isOwner available (require set in middleWare) + .get(subscriptions.get) // get + .put(model.isValid(subscriptionsSchema.Subscription), subscriptions.update) // update + .delete(model.isValid(subscriptionsSchema.Subscription), subscriptions.delete); // delete + + // Finish by binding the subscription middleware + app.param('subscriptionId', subscriptions.subscriptionByID); +}; diff --git a/modules/subscriptions/services/subscriptions.service.js b/modules/subscriptions/services/subscriptions.service.js new file mode 100644 index 000000000..43a19f5b8 --- /dev/null +++ b/modules/subscriptions/services/subscriptions.service.js @@ -0,0 +1,55 @@ +/** + * Module dependencies + */ +const SubscriptionsRepository = require('../repositories/subscriptions.repository'); + +/** + * @desc Function to get all subscription in db + * @return {Promise} All subscriptions + */ +exports.list = async () => { + const result = await SubscriptionsRepository.list(); + return Promise.resolve(result); +}; + +/** + * @desc Function to ask repository to create a subscription + * @param {Object} subscription + * @return {Promise} subscription + */ +exports.create = async (subscription) => { + const result = await SubscriptionsRepository.create(subscription); + return Promise.resolve(result); +}; + +/** + * @desc Function to ask repository to get a subscription + * @param {String} id + * @return {Promise} subscription + */ +exports.get = async (id) => { + const result = await SubscriptionsRepository.get(id); + return Promise.resolve(result); +}; + +/** + * @desc Functio to ask repository to update a subscription + * @param {Object} subscription - original subscription + * @param {Object} body - subscription edited + * @return {Promise} subscription + */ +exports.update = async (subscription, body) => { + subscription.email = body.email; + const result = await SubscriptionsRepository.update(subscription); + return Promise.resolve(result); +}; + +/** + * @desc Function to ask repository to delete a subscription + * @param {Object} subscription + * @return {Promise} confirmation of delete + */ +exports.delete = async (subscription) => { + const result = await SubscriptionsRepository.delete(subscription); + return Promise.resolve(result); +}; diff --git a/modules/subscriptions/tests/subscriptions.crud.tests.js b/modules/subscriptions/tests/subscriptions.crud.tests.js new file mode 100644 index 000000000..f1a848122 --- /dev/null +++ b/modules/subscriptions/tests/subscriptions.crud.tests.js @@ -0,0 +1,351 @@ +/** + * Module dependencies. + */ +const request = require('supertest'); +const path = require('path'); +const _ = require('lodash'); + +const express = require(path.resolve('./lib/services/express')); +const mongooseService = require(path.resolve('./lib/services/mongoose')); +const multerService = require(path.resolve('./lib/services/multer')); + +/** + * Unit tests + */ +describe('Subscriptions CRUD Tests :', () => { + let UserService = null; + let app; + let agent; + let credentials; + let user; + let userEdited; + let _user; + let _userEdited; + let _subscriptions; + let subscription1; + let subscription2; + + // init + beforeAll(async () => { + try { + // init mongo + await mongooseService.connect(); + await multerService.storage(); + await mongooseService.loadModels(); + UserService = require(path.resolve('./modules/users/services/user.service')); + // init application + app = express.init(); + agent = request.agent(app); + } catch (err) { + console.log(err); + } + }); + + describe('Logout', () => { + beforeEach(async () => { + // subscriptions + _subscriptions = [{ + email: 'test1@gmail.com', + news: true, + }, { + email: 'test2@gmail.com', + news: true, + }]; + + // add a subscription + try { + const result = await agent.post('/api/subscriptions') + .send(_subscriptions[0]) + .expect(200); + subscription1 = result.body.data; + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should be able to save a subscription', async () => { + // add subscription + try { + const result = await agent.post('/api/subscriptions') + .send(_subscriptions[1]) + .expect(200); + subscription2 = result.body.data; + expect(result.body.type).toBe('success'); + expect(result.body.message).toBe('subscription created'); + expect(result.body.data.email).toBe(_subscriptions[1].email); + expect(result.body.data.news).toBe(_subscriptions[1].news); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to save a subscription with bad model', async () => { + // add subscription + try { + const result = await agent.post('/api/subscriptions') + .send({ + email: 2, + news: false, + }) + .expect(422); + expect(result.body.type).toBe('error'); + expect(result.body.message).toEqual('Schema validation error'); + expect(result.body.description).toBe('"email" must be a string. '); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should be able to get a subscription', async () => { + // delete subscription + try { + const result = await agent.get(`/api/subscriptions/${subscription2.id}`) + .expect(200); + expect(result.body.type).toBe('success'); + expect(result.body.message).toBe('subscription get'); + expect(result.body.data.id).toBe(subscription2.id); + expect(result.body.data.email).toBe(_subscriptions[1].email); + expect(result.body.data.news).toBe(_subscriptions[1].news); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to get a subscription with a bad mongoose id', async () => { + // delete subscription + try { + const result = await agent.get('/api/subscriptions/test') + .expect(404); + expect(result.body.type).toBe('error'); + expect(result.body.message).toBe('Not Found'); + expect(result.body.description).toBe('No Subscription with that identifier has been found'); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to get a subscription with a bad invented id', async () => { + // delete subscription + try { + const result = await agent.get('/api/subscriptions/waos56397898004243871228') + .expect(404); + expect(result.body.type).toBe('error'); + expect(result.body.message).toBe('Not Found'); + expect(result.body.description).toBe('No Subscription with that identifier has been found'); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should be able to update a subscription', async () => { + // edit subscription + try { + const result = await agent.put(`/api/subscriptions/${subscription2.id}`) + .send({ email: 'test3@gmail.com', news: true }) + .expect(200); + expect(result.body.type).toBe('success'); + expect(result.body.message).toBe('subscription updated'); + expect(result.body.data.id).toBe(subscription2.id); + expect(result.body.data.email).toBe('test3@gmail.com'); + expect(result.body.data.news).toBe(_subscriptions[0].news); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to double an email in subscriptions', async () => { + // edit subscription + try { + const result = await agent.put(`/api/subscriptions/${subscription2.id}`) + .send(_subscriptions[0]) + .expect(422); + expect(result.body.type).toBe('error'); + expect(result.body.message).toEqual('Unprocessable Entity'); + expect(result.body.description).toBe('Validation failed.'); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to update a subscription with a bad id', async () => { + // edit subscription + try { + const result = await agent.put('/api/subscriptions/test') + .send(_subscriptions[0]) + .expect(404); + expect(result.body.type).toBe('error'); + expect(result.body.message).toBe('Not Found'); + expect(result.body.description).toBe('No Subscription with that identifier has been found'); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should be able to delete a subscription', async () => { + // delete subscription + try { + const result = await agent.delete(`/api/subscriptions/${subscription2.id}`) + .expect(200); + expect(result.body.type).toBe('success'); + expect(result.body.message).toBe('subscription deleted'); + expect(result.body.data.id).toBe(subscription2.id); + expect(result.body.data.deletedCount).toBe(1); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + // check delete + try { + await agent.get(`/api/subscriptions/${subscription2.id}`) + .expect(404); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to delete a subscription with a bad id', async () => { + // edit subscription + try { + const result = await agent.delete(`/api/subscriptions/${subscription2.id}`) + .send(_subscriptions[0]) + .expect(404); + expect(result.body.type).toBe('error'); + expect(result.body.message).toBe('Not Found'); + expect(result.body.description).toBe('No Subscription with that identifier has been found'); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to get list of subscriptions as guest', async () => { + // get list + try { + const result = await agent.get('/api/subscriptions') + .expect(401); + expect(result.error.text).toBe('Unauthorized'); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + afterEach(async () => { + // del subscription + try { + await agent.delete(`/api/subscriptions/${subscription1.id}`) + .expect(200); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + }); + + describe('Login', () => { + beforeEach(async () => { + // user credentials + credentials = [{ + email: 'test@test.com', + password: 'W@os.jsI$Aw3$0m3', + }, { + email: 'test2@test.com', + password: 'W@os.jsI$Aw3$0m3', + }]; + + // user + _user = { + firstName: 'Full', + lastName: 'Name', + email: credentials.email, + password: credentials.password, + provider: 'local', + }; + _userEdited = _.clone(_user); + _userEdited.email = credentials[1].email; + _userEdited.password = credentials[1].password; + + // add user + try { + const result = await agent.post('/api/auth/signup') + .send(_user) + .expect(200); + user = result.body.user; + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to get list of subscriptions as a user', async () => { + // get list + try { + await agent.get('/api/subscriptions') + .expect(403); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + test('should not be able to get list of subscriptions as an admin', async () => { + _userEdited.roles = ['user', 'admin']; + + try { + const result = await agent.post('/api/auth/signup') + .send(_userEdited) + .expect(200); + userEdited = result.body.user; + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + + try { + const result = await agent.get('/api/subscriptions') + .expect(200); + expect(result.body.type).toBe('success'); + expect(result.body.message).toBe('subscription list'); + expect(result.body.data).toBeInstanceOf(Array); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + + try { + await UserService.delete(userEdited); + } catch (err) { + console.log(err); + expect(err).toBeFalsy(); + } + }); + + afterEach(async () => { + // del user + try { + await UserService.delete(user); + } catch (err) { + console.log(err); + } + }); + }); + + // Mongoose disconnect + afterAll(async () => { + try { + await mongooseService.disconnect(); + } catch (err) { + console.log(err); + } + }); +}); diff --git a/modules/subscriptions/tests/subscriptions.schema.tests.js b/modules/subscriptions/tests/subscriptions.schema.tests.js new file mode 100644 index 000000000..7034ebcda --- /dev/null +++ b/modules/subscriptions/tests/subscriptions.schema.tests.js @@ -0,0 +1,66 @@ +/** + * Module dependencies. + */ +const _ = require('lodash'); +const path = require('path'); + +const config = require(path.resolve('./config')); +const options = _.clone(config.joi.validationOptions); +const schema = require('../models/subscriptions.schema'); + +// Globals +let subscription; + +/** + * Unit tests + */ +describe('Subscriptions Schema Tests :', () => { + beforeEach(() => { + subscription = { + email: 'test@gmail.com', + news: true, + }; + }); + + test('should be valid a subscription example without problems', (done) => { + const result = schema.Subscription.validate(subscription, options); + expect(typeof result).toBe('object'); + expect(result.error).toBeFalsy(); + done(); + }); + + test('should be able to show an error when trying a schema without title', (done) => { + subscription.email = ''; + + const result = schema.Subscription.validate(subscription, options); + expect(typeof result).toBe('object'); + expect(result.error).toBeDefined(); + done(); + }); + + test('should be able to show an error when trying a schema without news', (done) => { + subscription.news = null; + + const result = schema.Subscription.validate(subscription, options); + expect(typeof result).toBe('object'); + expect(result.error).toBeDefined(); + done(); + }); + + test('should not show an error when trying a schema with user', (done) => { + subscription.user = '507f1f77bcf86cd799439011'; + + const result = schema.Subscription.validate(subscription, options); + expect(typeof result).toBe('object'); + expect(result.error).toBeFalsy(); + done(); + }); + + test('should be able remove unknown when trying a different schema', (done) => { + subscription.toto = ''; + + const result = schema.Subscription.validate(subscription, options); + expect(result.toto).toBeUndefined(); + done(); + }); +}); diff --git a/modules/tasks/tests/tasks.crud.tests.js b/modules/tasks/tests/tasks.crud.tests.js index 12b9a42c8..672da18ee 100644 --- a/modules/tasks/tests/tasks.crud.tests.js +++ b/modules/tasks/tests/tasks.crud.tests.js @@ -167,7 +167,7 @@ describe('Tasks CRUD Tests :', () => { } }); - test('should be able to update a task if', async () => { + test('should be able to update a task', async () => { // edit task try { const result = await agent.put(`/api/tasks/${task2.id}`)