diff --git a/.nvmrc b/.nvmrc index a6105673..e51c059b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v6.9.4 +v8.2.1 diff --git a/README.md b/README.md index e2790ac4..209b291a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Microservice to manage CRUD operations for all things Projects. ### Requirements * [docker-compose](https://docs.docker.com/compose/install/) - We use docker-compose for running dependencies locally. -* Nodejs 8.9.4 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version +* Nodejs 8.2.1 - consider using [nvm](https://github.com/creationix/nvm) or equivalent to manage your node version * Install [libpg](https://www.npmjs.com/package/pg-native) ### Steps to run locally diff --git a/src/permissions/copilotAndAbove.js b/src/permissions/copilotAndAbove.js index e5d5121a..54f41409 100644 --- a/src/permissions/copilotAndAbove.js +++ b/src/permissions/copilotAndAbove.js @@ -1,18 +1,45 @@ +import _ from 'lodash'; import util from '../util'; -import { MANAGER_ROLES, USER_ROLE } from '../constants'; +import { + PROJECT_MEMBER_ROLE, + ADMIN_ROLES, +} from '../constants'; +import models from '../models'; /** - * Permission to alloow copilot and above roles to perform certain operations + * Permission to allow copilot and above roles to perform certain operations + * - User with Topcoder admins roles should be able to perform the operations. + * - Project members with copilot and manager Project roles should be also able to perform the operations. * @param {Object} req the express request instance * @return {Promise} returns a promise */ module.exports = req => new Promise((resolve, reject) => { - const hasAccess = util.hasRoles(req, [...MANAGER_ROLES, USER_ROLE.COPILOT]); + const projectId = _.parseInt(req.params.projectId); + const isAdmin = util.hasRoles(req, ADMIN_ROLES); - if (!hasAccess) { - return reject(new Error('You do not have permissions to perform this action')); + if (isAdmin) { + return resolve(true); } - return resolve(true); + return models.ProjectMember.getActiveProjectMembers(projectId) + .then((members) => { + req.context = req.context || {}; + req.context.currentProjectMembers = members; + const validMemberProjectRoles = [ + PROJECT_MEMBER_ROLE.MANAGER, + PROJECT_MEMBER_ROLE.COPILOT, + ]; + // check if the copilot or manager has access to this project + const isMember = _.some( + members, +m => m.userId === req.authUser.userId && validMemberProjectRoles.includes(m.role), + ); + + if (!isMember) { + // the copilot or manager is not a registered project member + return reject(new Error('You do not have permissions to perform this action')); + } + return resolve(true); + }); }); diff --git a/src/routes/milestoneTemplates/clone.js b/src/routes/milestoneTemplates/clone.js index bfe95a46..147f6dd1 100644 --- a/src/routes/milestoneTemplates/clone.js +++ b/src/routes/milestoneTemplates/clone.js @@ -30,7 +30,7 @@ module.exports = [ (req, res, next) => { let result; - return models.sequelize.transaction(tx => + return models.sequelize.transaction(() => // Find the product template models.MilestoneTemplate.findAll({ where: { @@ -48,7 +48,7 @@ module.exports = [ milestone.createdBy = req.authUser.userId; // eslint-disable-line no-param-reassign milestone.updatedBy = req.authUser.userId; // eslint-disable-line no-param-reassign }); - return models.MilestoneTemplate.bulkCreate(newMilestoneTemplates, { transaction: tx }); + return models.MilestoneTemplate.bulkCreate(newMilestoneTemplates); }) .then(() => { // eslint-disable-line arrow-body-style return models.MilestoneTemplate.findAll({ diff --git a/src/routes/milestoneTemplates/create.js b/src/routes/milestoneTemplates/create.js index 321c9b65..32ab860a 100644 --- a/src/routes/milestoneTemplates/create.js +++ b/src/routes/milestoneTemplates/create.js @@ -51,9 +51,9 @@ module.exports = [ }); let result; - return models.sequelize.transaction(tx => + return models.sequelize.transaction(() => // Create the milestone template - models.MilestoneTemplate.create(entity, { transaction: tx }) + models.MilestoneTemplate.create(entity) .then((createdEntity) => { // Omit deletedAt and deletedBy result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'); @@ -67,7 +67,6 @@ module.exports = [ id: { $ne: result.id }, order: { $gte: result.order }, }, - transaction: tx, }); }), ) diff --git a/src/routes/milestones/create.js b/src/routes/milestones/create.js index eadec1f4..e7a713e7 100644 --- a/src/routes/milestones/create.js +++ b/src/routes/milestones/create.js @@ -73,9 +73,9 @@ module.exports = [ return next(apiErr); } - return models.sequelize.transaction(tx => + return models.sequelize.transaction(() => // Save to DB - models.Milestone.create(entity, { transaction: tx }) + models.Milestone.create(entity) .then((createdEntity) => { // Omit deletedAt, deletedBy result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'); @@ -88,7 +88,6 @@ module.exports = [ id: { $ne: result.id }, order: { $gte: result.order }, }, - transaction: tx, }); }), ) diff --git a/src/routes/milestones/delete.js b/src/routes/milestones/delete.js index 45e5a41b..c2cf2bce 100644 --- a/src/routes/milestones/delete.js +++ b/src/routes/milestones/delete.js @@ -29,11 +29,10 @@ module.exports = [ id: req.params.milestoneId, }; - return models.sequelize.transaction(tx => + return models.sequelize.transaction(() => // Find the milestone models.Milestone.findOne({ where, - transaction: tx, }) .then((milestone) => { // Not found @@ -44,8 +43,8 @@ module.exports = [ } // Update the deletedBy, and soft delete - return milestone.update({ deletedBy: req.authUser.userId }, { transaction: tx }) - .then(() => milestone.destroy({ transaction: tx })); + return milestone.update({ deletedBy: req.authUser.userId }) + .then(() => milestone.destroy()); }), ) .then((deleted) => { diff --git a/src/routes/phaseProducts/create.spec.js b/src/routes/phaseProducts/create.spec.js index 8f81a28b..b8b462f8 100644 --- a/src/routes/phaseProducts/create.spec.js +++ b/src/routes/phaseProducts/create.spec.js @@ -177,7 +177,7 @@ describe('Phase Products', () => { request(server) .post(`/v4/projects/99999/phases/${phaseId}/products`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) .send({ param: body }) .expect('Content-Type', /json/) @@ -188,7 +188,7 @@ describe('Phase Products', () => { request(server) .post(`/v4/projects/${projectId}/phases/99999/products`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) .send({ param: body }) .expect('Content-Type', /json/) @@ -220,6 +220,68 @@ describe('Phase Products', () => { }); }); + it('should return 201 if requested by admin', (done) => { + request(server) + .post(`/v4/projects/${projectId}/phases/${phaseId}/products`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(201) + .end(done); + }); + + it('should return 201 if requested by manager which is a member', (done) => { + models.ProjectMember.create({ + id: 3, + userId: testUtil.userIds.manager, + projectId, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then(() => { + request(server) + .post(`/v4/projects/${projectId}/phases/${phaseId}/products`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(201) + .end(done); + }); + }); + + it('should return 403 if requested by manager which is not a member', (done) => { + request(server) + .post(`/v4/projects/${projectId}/phases/${phaseId}/products`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + + it('should return 403 if requested by non-member copilot', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, projectId }, + }).then(() => { + request(server) + .post(`/v4/projects/${projectId}/phases/${phaseId}/products`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + }); + describe('Bus api', () => { let createEventSpy; const sandbox = sinon.sandbox.create(); diff --git a/src/routes/phaseProducts/delete.spec.js b/src/routes/phaseProducts/delete.spec.js index 69942fa6..03db9a9d 100644 --- a/src/routes/phaseProducts/delete.spec.js +++ b/src/routes/phaseProducts/delete.spec.js @@ -156,7 +156,7 @@ describe('Phase Products', () => { request(server) .delete(`/v4/projects/999/phases/${phaseId}/products/${productId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) .expect('Content-Type', /json/) .expect(404, done); @@ -166,7 +166,7 @@ describe('Phase Products', () => { request(server) .delete(`/v4/projects/${projectId}/phases/99999/products/${productId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) .expect('Content-Type', /json/) .expect(404, done); @@ -176,7 +176,7 @@ describe('Phase Products', () => { request(server) .delete(`/v4/projects/${projectId}/phases/${phaseId}/products/99999`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) .expect('Content-Type', /json/) .expect(404, done); @@ -192,6 +192,60 @@ describe('Phase Products', () => { .end(err => expectAfterDelete(projectId, phaseId, productId, err, done)); }); + it('should return 204 if requested by admin', (done) => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .expect(204) + .end(done); + }); + + it('should return 204 if requested by manager which is a member', (done) => { + models.ProjectMember.create({ + id: 3, + userId: testUtil.userIds.manager, + projectId, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then(() => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(204) + .end(done); + }); + }); + + it('should return 403 if requested by manager which is not a member', (done) => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403) + .end(done); + }); + + it('should return 403 if requested by non-member copilot', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, projectId }, + }).then(() => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403) + .end(done); + }); + }); + describe('Bus api', () => { let createEventSpy; const sandbox = sinon.sandbox.create(); diff --git a/src/routes/phaseProducts/update.spec.js b/src/routes/phaseProducts/update.spec.js index 3c35871b..5dcb8771 100644 --- a/src/routes/phaseProducts/update.spec.js +++ b/src/routes/phaseProducts/update.spec.js @@ -51,7 +51,7 @@ describe('Phase Products', () => { lastName: 'lName', email: 'some@abc.com', }; - before((done) => { + beforeEach((done) => { // mocks testUtil.clearDb() .then(() => { @@ -144,7 +144,7 @@ describe('Phase Products', () => { request(server) .patch(`/v4/projects/999/phases/${phaseId}/products/${productId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, }) .send({ param: updateBody }) .expect('Content-Type', /json/) @@ -155,7 +155,7 @@ describe('Phase Products', () => { request(server) .patch(`/v4/projects/${projectId}/phases/99999/products/${productId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ param: updateBody }) .expect('Content-Type', /json/) @@ -166,7 +166,7 @@ describe('Phase Products', () => { request(server) .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/99999`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ param: updateBody }) .expect('Content-Type', /json/) @@ -177,7 +177,7 @@ describe('Phase Products', () => { request(server) .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/99999`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.copilot}`, }) .send({ param: { @@ -214,6 +214,68 @@ describe('Phase Products', () => { }); }); + it('should return 200 if requested by admin', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.connectAdmin}`, + }) + .send({ param: updateBody }) + .expect('Content-Type', /json/) + .expect(200) + .end(done); + }); + + it('should return 200 if requested by manager which is a member', (done) => { + models.ProjectMember.create({ + id: 3, + userId: testUtil.userIds.manager, + projectId, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then(() => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: updateBody }) + .expect('Content-Type', /json/) + .expect(200) + .end(done); + }); + }); + + it('should return 403 if requested by manager which is not a member', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: updateBody }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + + it('should return 403 if requested by non-member copilot', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, projectId }, + }).then(() => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}/products/${productId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ param: updateBody }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + }); + describe('Bus api', () => { let createEventSpy; const sandbox = sinon.sandbox.create(); diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js index 69f45a4d..d36cdb4c 100644 --- a/src/routes/phases/create.spec.js +++ b/src/routes/phases/create.spec.js @@ -54,44 +54,42 @@ describe('Project Phases', () => { email: 'some@abc.com', }; let productTemplateId; - before((done) => { + beforeEach((done) => { // mocks testUtil.clearDb() - .then(() => { - models.Project.create({ - type: 'generic', - billingAccountId: 1, - name: 'test1', - description: 'test project1', - status: 'draft', - details: {}, + .then(() => models.Project.create({ + type: 'generic', + billingAccountId: 1, + name: 'test1', + description: 'test project1', + status: 'draft', + details: {}, + createdBy: 1, + updatedBy: 1, + lastActivityAt: 1, + lastActivityUserId: '1', + }).then((p) => { + projectId = p.id; + projectName = p.name; + // create members + return models.ProjectMember.bulkCreate([{ + id: 1, + userId: copilotUser.userId, + projectId, + role: 'copilot', + isPrimary: false, createdBy: 1, updatedBy: 1, - lastActivityAt: 1, - lastActivityUserId: '1', - }).then((p) => { - projectId = p.id; - projectName = p.name; - // create members - models.ProjectMember.bulkCreate([{ - id: 1, - userId: copilotUser.userId, - projectId, - role: 'copilot', - isPrimary: false, - createdBy: 1, - updatedBy: 1, - }, { - id: 2, - userId: memberUser.userId, - projectId, - role: 'customer', - isPrimary: true, - createdBy: 1, - updatedBy: 1, - }]); - }); - }) + }, { + id: 2, + userId: memberUser.userId, + projectId, + role: 'customer', + isPrimary: true, + createdBy: 1, + updatedBy: 1, + }]); + })) .then(() => models.ProductTemplate.create({ name: 'name 1', @@ -128,7 +126,7 @@ describe('Project Phases', () => { .then(() => done()); }); - after((done) => { + afterEach((done) => { testUtil.clearDb(done); }); @@ -224,7 +222,7 @@ describe('Project Phases', () => { request(server) .post('/v4/projects/99999/phases/') .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ param: body }) .expect('Content-Type', /json/) @@ -347,6 +345,68 @@ describe('Project Phases', () => { }); }); + it('should return 201 if requested by admin', (done) => { + request(server) + .post(`/v4/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(201) + .end(done); + }); + + it('should return 201 if requested by manager which is a member', (done) => { + models.ProjectMember.create({ + id: 3, + userId: testUtil.userIds.manager, + projectId, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then(() => { + request(server) + .post(`/v4/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(201) + .end(done); + }); + }); + + it('should return 403 if requested by manager which is not a member', (done) => { + request(server) + .post(`/v4/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + + it('should return 403 if requested by non-member copilot', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, projectId }, + }).then(() => { + request(server) + .post(`/v4/projects/${projectId}/phases/`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ param: body }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + }); + describe('Bus api', () => { let createEventSpy; const sandbox = sinon.sandbox.create(); diff --git a/src/routes/phases/delete.spec.js b/src/routes/phases/delete.spec.js index 78453f39..bea3d2be 100644 --- a/src/routes/phases/delete.spec.js +++ b/src/routes/phases/delete.spec.js @@ -145,7 +145,7 @@ describe('Project Phases', () => { request(server) .delete(`/v4/projects/999/phases/${phaseId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .expect('Content-Type', /json/) .expect(404, done); @@ -155,7 +155,7 @@ describe('Project Phases', () => { request(server) .delete(`/v4/projects/${projectId}/phases/999`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .expect('Content-Type', /json/) .expect(404, done); @@ -167,9 +167,64 @@ describe('Project Phases', () => { .set({ Authorization: `Bearer ${testUtil.jwts.copilot}`, }) + .expect(204) .end(err => expectAfterDelete(projectId, phaseId, err, done)); }); + it('should return 204 if requested by admin', (done) => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .expect(204) + .end(done); + }); + + it('should return 204 if requested by manager which is a member', (done) => { + models.ProjectMember.create({ + id: 3, + userId: testUtil.userIds.manager, + projectId, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then(() => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(204) + .end(done); + }); + }); + + it('should return 403 if requested by manager which is not a member', (done) => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .expect(403) + .end(done); + }); + + it('should return 403 if requested by non-member copilot', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, projectId }, + }).then(() => { + request(server) + .delete(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .expect(403) + .end(done); + }); + }); + describe('Bus api', () => { let createEventSpy; const sandbox = sinon.sandbox.create(); diff --git a/src/routes/phases/update.spec.js b/src/routes/phases/update.spec.js index 85e8e44d..7938be9e 100644 --- a/src/routes/phases/update.spec.js +++ b/src/routes/phases/update.spec.js @@ -68,7 +68,7 @@ describe('Project Phases', () => { lastName: 'lName', email: 'some@abc.com', }; - before((done) => { + beforeEach((done) => { // mocks testUtil.clearDb() .then(() => { @@ -121,7 +121,7 @@ describe('Project Phases', () => { }); }); - after((done) => { + afterEach((done) => { testUtil.clearDb(done); }); @@ -152,7 +152,7 @@ describe('Project Phases', () => { request(server) .patch(`/v4/projects/999/phases/${phaseId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ param: updateBody }) .expect('Content-Type', /json/) @@ -163,7 +163,7 @@ describe('Project Phases', () => { request(server) .patch(`/v4/projects/${projectId}/phases/999`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ param: updateBody }) .expect('Content-Type', /json/) @@ -174,7 +174,7 @@ describe('Project Phases', () => { request(server) .patch(`/v4/projects/${projectId}/phases/${phaseId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ param: { @@ -189,7 +189,7 @@ describe('Project Phases', () => { request(server) .patch(`/v4/projects/${projectId}/phases/${phaseId}`) .set({ - Authorization: `Bearer ${testUtil.jwts.manager}`, + Authorization: `Bearer ${testUtil.jwts.admin}`, }) .send({ param: { @@ -272,6 +272,68 @@ describe('Project Phases', () => { }); }); + it('should return 200 if requested by admin', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.admin}`, + }) + .send({ param: _.assign({ order: 1 }, updateBody) }) + .expect('Content-Type', /json/) + .expect(200) + .end(done); + }); + + it('should return 200 if requested by manager which is a member', (done) => { + models.ProjectMember.create({ + id: 3, + userId: testUtil.userIds.manager, + projectId, + role: 'manager', + isPrimary: false, + createdBy: 1, + updatedBy: 1, + }).then(() => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: _.assign({ order: 1 }, updateBody) }) + .expect('Content-Type', /json/) + .expect(200) + .end(done); + }); + }); + + it('should return 403 if requested by manager which is not a member', (done) => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.manager}`, + }) + .send({ param: _.assign({ order: 1 }, updateBody) }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + + it('should return 403 if requested by non-member copilot', (done) => { + models.ProjectMember.destroy({ + where: { userId: testUtil.userIds.copilot, projectId }, + }).then(() => { + request(server) + .patch(`/v4/projects/${projectId}/phases/${phaseId}`) + .set({ + Authorization: `Bearer ${testUtil.jwts.copilot}`, + }) + .send({ param: _.assign({ order: 1 }, updateBody) }) + .expect('Content-Type', /json/) + .expect(403) + .end(done); + }); + }); + describe('Bus api', () => { let createEventSpy; const sandbox = sinon.sandbox.create(); diff --git a/src/routes/productCategories/create.js b/src/routes/productCategories/create.js index e80bc596..152a7d2e 100644 --- a/src/routes/productCategories/create.js +++ b/src/routes/productCategories/create.js @@ -41,10 +41,10 @@ module.exports = [ }); // Check if duplicated key - return models.ProductCategory.findById(req.body.param.key) + return models.ProductCategory.findById(req.body.param.key, { paranoid: false }) .then((existing) => { if (existing) { - const apiErr = new Error(`Product category already exists for key ${req.params.key}`); + const apiErr = new Error(`Product category already exists (may be deleted) for key "${req.body.param.key}"`); apiErr.status = 422; return Promise.reject(apiErr); } diff --git a/src/routes/projectTypes/create.js b/src/routes/projectTypes/create.js index 8e73e1ec..15cf3d49 100644 --- a/src/routes/projectTypes/create.js +++ b/src/routes/projectTypes/create.js @@ -42,10 +42,10 @@ module.exports = [ }); // Check if duplicated key - return models.ProjectType.findById(req.body.param.key) + return models.ProjectType.findById(req.body.param.key, { paranoid: false }) .then((existing) => { if (existing) { - const apiErr = new Error(`Project type already exists for key ${req.params.key}`); + const apiErr = new Error(`Project type already exists (may be deleted) for key "${req.body.param.key}"`); apiErr.status = 422; return Promise.reject(apiErr); } diff --git a/src/routes/timelines/create.js b/src/routes/timelines/create.js index a37b7adb..b001f2f4 100644 --- a/src/routes/timelines/create.js +++ b/src/routes/timelines/create.js @@ -51,9 +51,9 @@ module.exports = [ let result; // Save to DB - models.sequelize.transaction((tx) => { + return models.sequelize.transaction(() => { req.log.debug('Started transaction'); - return models.Timeline.create(entity, { transaction: tx }) + return models.Timeline.create(entity) .then((createdEntity) => { // Omit deletedAt, deletedBy result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'); @@ -97,7 +97,7 @@ module.exports = [ } return milestone; }); - return models.Milestone.bulkCreate(milestones, { returning: true, transaction: tx }) + return models.Milestone.bulkCreate(milestones, { returning: true }) .then((createdMilestones) => { req.log.debug('Milestones created for timeline with template id %d', templateId); result.milestones = _.map(createdMilestones, cm => _.omit(cm.toJSON(), 'deletedAt', 'deletedBy')); @@ -109,8 +109,7 @@ module.exports = [ }); } return Promise.resolve(); - }) - .catch(next); + }); }) .then(() => { // Send event to bus